├── GUI ├── EQ │ └── __init__.py ├── __init__.py ├── Misc │ ├── __init__.py │ ├── colorStr.py │ ├── filemanager_name.py │ ├── adg_thread_run.py │ ├── TextBrowserDocParameters.py │ ├── StartScreen.py │ └── error_message.py ├── Modes │ ├── __init__.py │ ├── audiosource_modes.py │ └── TestMode.py ├── EQSettings │ ├── __init__.py │ ├── eqset_view.py │ └── eqset_contr.py ├── ExScoreInfo │ ├── __init__.py │ ├── exscoreinfo_view.py │ └── exscoreinfo_contr.py ├── FileMaker │ ├── __init__.py │ ├── FileCreationSuccessDialog.py │ └── make_playlist.py ├── MainWindow │ ├── __init__.py │ ├── Contr │ │ ├── __init__.py │ │ ├── learn_freq_order_handler.py │ │ ├── adgen_contr.py │ │ └── sourcerange_contr.py │ └── View │ │ ├── __init__.py │ │ └── audiodevices_view.py ├── PatternBox │ ├── __init__.py │ ├── patternbox_view.py │ └── patternbox_contr.py ├── Playlist │ ├── __init__.py │ ├── PLLoadDialog.py │ ├── ContextMenu.py │ └── plsong.py ├── SupportApp │ ├── __init__.py │ └── supportapp_contr.py ├── AudioProcSettings │ ├── __init__.py │ ├── eq_indicator_view.py │ ├── audio_proc_settings_contr.py │ └── audio_proc_settings_view.py ├── TransportPanel │ ├── __init__.py │ ├── cropregiontimestr.py │ ├── player_view.py │ ├── volumeslider_contr.py │ └── transport_view.py ├── UpdateChecker │ ├── __init__.py │ ├── update_checker_runner.py │ └── update_checker_contr.py ├── AudioConvertDialog │ ├── __init__.py │ └── convert_dialog_contr.py ├── MakeLearnTestFiles │ └── __init__.py ├── Help │ ├── __init__.py │ ├── Data │ │ └── Images │ │ │ ├── C4_A4.png │ │ │ ├── clear.png │ │ │ ├── BW_form.png │ │ │ ├── TestMode.png │ │ │ ├── star_16.png │ │ │ ├── BWoct_form.png │ │ │ ├── LearnMode.png │ │ │ ├── lightbulb.png │ │ │ ├── youtube_16.png │ │ │ ├── PreviewMode.png │ │ │ ├── next-example.png │ │ │ ├── settings_16.png │ │ │ ├── Drill_structure.png │ │ │ ├── Frequency-Note.png │ │ │ ├── Q_from_BW_form1.png │ │ │ ├── Q_from_BW_form2.png │ │ │ ├── TransportPanel.png │ │ │ ├── BellFilterScheme.png │ │ │ ├── sequential-playback.png │ │ │ ├── media-skip-backward_16.png │ │ │ ├── media-skip-forward_16.png │ │ │ ├── EQ_Frequencies_Help_QRcode.png │ │ │ └── arrows-right-and-left-filled-triangles.png │ ├── help_img.qrc │ └── HelpActions.py ├── Icons │ ├── Misc │ │ ├── star.png │ │ ├── unlock.png │ │ ├── Settings.png │ │ ├── padlock.png │ │ ├── Negative │ │ │ ├── star.png │ │ │ ├── padlock.png │ │ │ ├── unlock.png │ │ │ └── Settings.png │ │ └── next-pattern.png │ ├── AddRemove │ │ ├── plus.png │ │ ├── clear.png │ │ ├── minus.png │ │ ├── clear_neg.png │ │ └── clear_neg-disabled.png │ ├── Support_Logos │ │ ├── vk.png │ │ ├── Boosty.png │ │ ├── Patreon.png │ │ ├── linked.png │ │ ├── reddit.png │ │ ├── twitter.png │ │ ├── WhatsApp.png │ │ ├── bmc-logo.png │ │ ├── facebook.png │ │ ├── linked_neg.png │ │ └── telegram.png │ ├── Logo │ │ ├── EarQuiz_Distr.png │ │ ├── EarQuiz_Header.png │ │ ├── EarQuiz_Icon.icns │ │ ├── EarQuiz_Icon.ico │ │ ├── EarQuiz_Icon.png │ │ ├── EarQuiz_Logo.png │ │ └── EarQuiz_Splash.png │ └── Player │ │ ├── CurrentSong.png │ │ ├── shuffle_blue.png │ │ ├── shuffle_black.png │ │ ├── arrow-right_gray.png │ │ ├── sequence - black.png │ │ ├── sequence - blue.png │ │ ├── left-arrow-playlist.png │ │ ├── Negative │ │ ├── shuffle_black.png │ │ ├── arrow-right_gray.png │ │ ├── sequence - black.png │ │ ├── left-arrow-playlist.png │ │ ├── right-arrow-playlist.png │ │ ├── shuffle_black-disabled.png │ │ ├── arrow-right_gray-disabled.png │ │ ├── left-arrow-playlist-disabled.png │ │ ├── right-arrow-playlist-disabled.png │ │ └── music-note-with-loop-circular-arrows-around.png │ │ ├── right-arrow-playlist.png │ │ ├── shuffle_blue-disabled.png │ │ ├── Actions-media-playback-stop-icon.png │ │ ├── Actions-media-seek-backward-icon.png │ │ ├── Actions-media-seek-forward-icon.png │ │ ├── Actions-media-skip-backward-icon.png │ │ ├── Actions-media-skip-forward-icon.png │ │ ├── Actions-media-playback-pause-icon.png │ │ ├── Actions-media-playback-start-icon.png │ │ ├── music-note-with-loop-circular-arrows-around.png │ │ └── music-note-with-loop-circular-arrows-around-blue.png ├── globals.py ├── About │ ├── about_dialog_view.py │ └── credits.md └── icons.qrc ├── Model ├── __init__.py ├── AudioEngine │ ├── __init__.py │ ├── audio_to_buffer.py │ ├── sine_wav_gen.py │ ├── audio_proc_settings.py │ ├── pinknoise_gen.py │ ├── convert_audio.py │ └── preview_audio.py ├── Data │ ├── bw_q_patterns.json │ └── eq_patterns.json ├── Version │ └── version.json ├── globals.py ├── file_hash.py ├── calc.py ├── get_version.py ├── eq_patterns.py ├── export_playlist.py ├── sourcerange_manager.py └── scorecalc.py ├── Utilities ├── __init__.py ├── exceptions.py ├── Q_extract.py ├── checkMimeData.py ├── str2bool.py ├── urlcheck.py ├── freq2str.py ├── selectFileInSysExplorer.py └── common_calcs.py ├── pb_init.py ├── requirements.txt ├── Distribution ├── Linux │ ├── package │ │ └── usr │ │ │ └── share │ │ │ ├── icons │ │ │ └── hicolor │ │ │ │ ├── 16x16 │ │ │ │ └── apps │ │ │ │ │ └── EarQuiz_Icon.png │ │ │ │ ├── 24x24 │ │ │ │ └── apps │ │ │ │ │ └── EarQuiz_Icon.png │ │ │ │ ├── 32x32 │ │ │ │ └── apps │ │ │ │ │ └── EarQuiz_Icon.png │ │ │ │ ├── 64x64 │ │ │ │ └── apps │ │ │ │ │ └── EarQuiz_Icon.png │ │ │ │ ├── 96x96 │ │ │ │ └── apps │ │ │ │ │ └── EarQuiz_Icon.png │ │ │ │ ├── 128x128 │ │ │ │ └── apps │ │ │ │ │ └── EarQuiz_Icon.png │ │ │ │ ├── 256x256 │ │ │ │ └── apps │ │ │ │ │ └── EarQuiz_Icon.png │ │ │ │ └── 512x512 │ │ │ │ └── apps │ │ │ │ └── EarQuiz_Icon.png │ │ │ └── applications │ │ │ └── earquiz-frequencies.desktop │ └── .fpm └── Windows │ ├── README.txt │ └── eqfreq_AppVeyor.iss ├── env_vars.py ├── linux-package.sh ├── appveyor-macos-x64.yml ├── .github └── FUNDING.yml ├── linux_build.spec ├── windows_build.spec ├── version.rc ├── gui_compile.py ├── main.py ├── definitions.py ├── macos_build.spec ├── macos_build-arm64.spec ├── appveyor.yml └── application.py /GUI/EQ/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/Misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/Modes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Utilities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/EQSettings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/ExScoreInfo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/FileMaker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/MainWindow/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/PatternBox/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/Playlist/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/SupportApp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pb_init.py: -------------------------------------------------------------------------------- 1 | import pedalboard -------------------------------------------------------------------------------- /GUI/AudioProcSettings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/MainWindow/Contr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/TransportPanel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/UpdateChecker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Model/AudioEngine/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/AudioConvertDialog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/MakeLearnTestFiles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GUI/Help/__init__.py: -------------------------------------------------------------------------------- 1 | from GUI.Help import help_img_rc 2 | -------------------------------------------------------------------------------- /GUI/Icons/Misc/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/star.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/unlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/unlock.png -------------------------------------------------------------------------------- /GUI/Icons/AddRemove/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/AddRemove/plus.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/Settings.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/padlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/padlock.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/C4_A4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/C4_A4.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/clear.png -------------------------------------------------------------------------------- /GUI/Icons/AddRemove/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/AddRemove/clear.png -------------------------------------------------------------------------------- /GUI/Icons/AddRemove/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/AddRemove/minus.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/vk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/vk.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/BW_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/BW_form.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/TestMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/TestMode.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/star_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/star_16.png -------------------------------------------------------------------------------- /GUI/Icons/AddRemove/clear_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/AddRemove/clear_neg.png -------------------------------------------------------------------------------- /GUI/Icons/Logo/EarQuiz_Distr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Logo/EarQuiz_Distr.png -------------------------------------------------------------------------------- /GUI/Icons/Logo/EarQuiz_Header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Logo/EarQuiz_Header.png -------------------------------------------------------------------------------- /GUI/Icons/Logo/EarQuiz_Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Logo/EarQuiz_Icon.icns -------------------------------------------------------------------------------- /GUI/Icons/Logo/EarQuiz_Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Logo/EarQuiz_Icon.ico -------------------------------------------------------------------------------- /GUI/Icons/Logo/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Logo/EarQuiz_Icon.png -------------------------------------------------------------------------------- /GUI/Icons/Logo/EarQuiz_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Logo/EarQuiz_Logo.png -------------------------------------------------------------------------------- /GUI/Icons/Logo/EarQuiz_Splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Logo/EarQuiz_Splash.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/Negative/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/Negative/star.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/next-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/next-pattern.png -------------------------------------------------------------------------------- /GUI/Icons/Player/CurrentSong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/CurrentSong.png -------------------------------------------------------------------------------- /GUI/Icons/Player/shuffle_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/shuffle_blue.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/BWoct_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/BWoct_form.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/LearnMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/LearnMode.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/lightbulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/lightbulb.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/youtube_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/youtube_16.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/Negative/padlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/Negative/padlock.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/Negative/unlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/Negative/unlock.png -------------------------------------------------------------------------------- /GUI/Icons/Player/shuffle_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/shuffle_black.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/Boosty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/Boosty.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/Patreon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/Patreon.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/linked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/linked.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/reddit.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/twitter.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/PreviewMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/PreviewMode.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/next-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/next-example.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/settings_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/settings_16.png -------------------------------------------------------------------------------- /GUI/Icons/Misc/Negative/Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Misc/Negative/Settings.png -------------------------------------------------------------------------------- /GUI/Icons/Player/arrow-right_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/arrow-right_gray.png -------------------------------------------------------------------------------- /GUI/Icons/Player/sequence - black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/sequence - black.png -------------------------------------------------------------------------------- /GUI/Icons/Player/sequence - blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/sequence - blue.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/WhatsApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/WhatsApp.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/bmc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/bmc-logo.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/facebook.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/linked_neg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/linked_neg.png -------------------------------------------------------------------------------- /GUI/Icons/Support_Logos/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Support_Logos/telegram.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/Drill_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/Drill_structure.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/Frequency-Note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/Frequency-Note.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/Q_from_BW_form1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/Q_from_BW_form1.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/Q_from_BW_form2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/Q_from_BW_form2.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/TransportPanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/TransportPanel.png -------------------------------------------------------------------------------- /GUI/Icons/Player/left-arrow-playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/left-arrow-playlist.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/BellFilterScheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/BellFilterScheme.png -------------------------------------------------------------------------------- /GUI/Icons/AddRemove/clear_neg-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/AddRemove/clear_neg-disabled.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/shuffle_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/shuffle_black.png -------------------------------------------------------------------------------- /GUI/Icons/Player/right-arrow-playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/right-arrow-playlist.png -------------------------------------------------------------------------------- /GUI/Icons/Player/shuffle_blue-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/shuffle_blue-disabled.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/sequential-playback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/sequential-playback.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/media-skip-backward_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/media-skip-backward_16.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/media-skip-forward_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/media-skip-forward_16.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/arrow-right_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/arrow-right_gray.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/sequence - black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/sequence - black.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/left-arrow-playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/left-arrow-playlist.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/right-arrow-playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/right-arrow-playlist.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/EQ_Frequencies_Help_QRcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/EQ_Frequencies_Help_QRcode.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Actions-media-playback-stop-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Actions-media-playback-stop-icon.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Actions-media-seek-backward-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Actions-media-seek-backward-icon.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Actions-media-seek-forward-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Actions-media-seek-forward-icon.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Actions-media-skip-backward-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Actions-media-skip-backward-icon.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Actions-media-skip-forward-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Actions-media-skip-forward-icon.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/shuffle_black-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/shuffle_black-disabled.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Actions-media-playback-pause-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Actions-media-playback-pause-icon.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Actions-media-playback-start-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Actions-media-playback-start-icon.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/arrow-right_gray-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/arrow-right_gray-disabled.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/left-arrow-playlist-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/left-arrow-playlist-disabled.png -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/right-arrow-playlist-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/right-arrow-playlist-disabled.png -------------------------------------------------------------------------------- /GUI/Help/Data/Images/arrows-right-and-left-filled-triangles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Help/Data/Images/arrows-right-and-left-filled-triangles.png -------------------------------------------------------------------------------- /GUI/Icons/Player/music-note-with-loop-circular-arrows-around.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/music-note-with-loop-circular-arrows-around.png -------------------------------------------------------------------------------- /GUI/Icons/Player/music-note-with-loop-circular-arrows-around-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/music-note-with-loop-circular-arrows-around-blue.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pedalboard 2 | pyqtgraph~=0.13.4 3 | numpy 4 | PyQt6~=6.8.0; sys_platform == 'win32' or sys_platform == 'darwin' 5 | PyQt6~=6.7.1; sys_platform == 'linux' 6 | tendo~=0.3.0 7 | certifi -------------------------------------------------------------------------------- /GUI/Icons/Player/Negative/music-note-with-loop-circular-arrows-around.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/GUI/Icons/Player/Negative/music-note-with-loop-circular-arrows-around.png -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/16x16/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/16x16/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/24x24/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/24x24/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/32x32/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/32x32/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/64x64/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/64x64/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/96x96/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/96x96/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /Model/Data/bw_q_patterns.json: -------------------------------------------------------------------------------- 1 | [ 2 | "1 Oct (Q=1.41)", 3 | "3/4 Oct (Q=1.9)", 4 | "2/3 Oct (Q=2.14)", 5 | "1/2 Oct (Q=2.87)", 6 | "1/3 Oct (Q=4.32)", 7 | "1/4 Oct (Q=5.76)", 8 | "1/6 Oct (Q=8.65)" 9 | ] -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/128x128/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/128x128/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/256x256/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/256x256/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/icons/hicolor/512x512/apps/EarQuiz_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gdalik/EarQuiz_Frequencies/HEAD/Distribution/Linux/package/usr/share/icons/hicolor/512x512/apps/EarQuiz_Icon.png -------------------------------------------------------------------------------- /env_vars.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | os.environ['QT_MEDIA_BACKEND'] = 'ffmpeg' 5 | os.environ['QT_LOGGING_RULES'] = '*.multimedia.*=false' 6 | os.environ['QT_FFMPEG_DECODING_HW_DEVICE_TYPES'] = ',' 7 | os.environ['QT_FFMPEG_ENCODING_HW_DEVICE_TYPES'] = ',' 8 | -------------------------------------------------------------------------------- /Model/Version/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": { 3 | "major": 0, 4 | "minor": 1, 5 | "patch": 8 6 | }, 7 | "info_file": "", 8 | "update_active": false, 9 | "download_mac": "", 10 | "download_mac-arm64": "", 11 | "download_win": "", 12 | "download_linux": "" 13 | } -------------------------------------------------------------------------------- /Distribution/Linux/.fpm: -------------------------------------------------------------------------------- 1 | -C package 2 | -s dir 3 | -t deb 4 | -n "earquiz-frequencies" 5 | -v 0.1.8 6 | -p earquiz-frequencies_0.1.8-01_amd64.deb 7 | --license GPL-3.0+ 8 | --architecture amd64 9 | --description "Software for ear training on equalization" 10 | --url "https://earquiz.org/EQ_Frequencies/" 11 | --maintainer "Gdaliy Garmiza " 12 | --vendor EarQuiz 13 | 14 | -------------------------------------------------------------------------------- /Model/AudioEngine/audio_to_buffer.py: -------------------------------------------------------------------------------- 1 | import io 2 | import numpy as np 3 | from pedalboard.io import AudioFile 4 | 5 | 6 | def a2b(audio_data: np.array, samplerate: int) -> io.BytesIO: 7 | res = io.BytesIO() 8 | with AudioFile(res, "w", samplerate=samplerate, num_channels=audio_data.shape[0], format='wav') as file_obj: 9 | file_obj.write(audio_data) 10 | return res 11 | -------------------------------------------------------------------------------- /linux-package.sh: -------------------------------------------------------------------------------- 1 | mkdir -p Distribution/Linux/package/opt && cp -r dist/earquiz-frequencies Distribution/Linux/package/opt 2 | 3 | # Change permissions 4 | find Distribution/Linux/package/opt/earquiz-frequencies -type f -exec chmod 644 -- {} + 5 | find Distribution/Linux/package/opt/earquiz-frequencies -type d -exec chmod 755 -- {} + 6 | find Distribution/Linux/package/usr/share -type f -exec chmod 644 -- {} + 7 | chmod +x Distribution/Linux/package/opt/earquiz-frequencies/earquiz-frequencies 8 | -------------------------------------------------------------------------------- /Distribution/Linux/package/usr/share/applications/earquiz-frequencies.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | 3 | Type=Application 4 | Name=EarQuiz Frequencies 5 | Comment=Software for technical ear training on equalization 6 | Path=/opt/earquiz-frequencies 7 | Exec=/opt/earquiz-frequencies/earquiz-frequencies 8 | Icon=EarQuiz_Icon 9 | MimeType=audio/x-scpls;application/xspf+xml;audio/x-mpegurl;audio/mpegurl;application/x-mpegurl;application/vnd.apple.mpegurl;audio/scpls;application/pls+xml;audio/wav;audio/x-wav;audio/mpeg;audio/x-mpeg;audio/aiff;audio/x-aiff;audio/flac;audio/x-flac;audio/ogg;audio/x-ogg 10 | -------------------------------------------------------------------------------- /appveyor-macos-x64.yml: -------------------------------------------------------------------------------- 1 | image: macos-monterey 2 | 3 | platform: x64 4 | 5 | init: 6 | - chmod +x $HOME/venv3.12/bin/activate 7 | - source $HOME/venv3.12/bin/activate 8 | 9 | install: 10 | - python -m pip install --upgrade pip 11 | - pip install -r requirements.txt 12 | - pip install -U pyinstaller 13 | - pyinstaller macos_build.spec 14 | - tar -cvf eqfreq_build-macos-x64.tar "dist/EarQuiz Frequencies.app" 15 | 16 | build: None 17 | 18 | artifacts: 19 | - path: eqfreq_build-macos-x64.tar 20 | 21 | version: 0.1.8 (build-{build}) 22 | pull_requests: 23 | do_not_increment_build_number: true 24 | -------------------------------------------------------------------------------- /Distribution/Windows/README.txt: -------------------------------------------------------------------------------- 1 | EarQuiz Frequencies v0.1.8. Software for technical ear training on equalization. 2 | Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: EarQuiz # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://boosty.to/earquiz'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /Utilities/exceptions.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | class InterruptedException(Exception): 18 | pass 19 | -------------------------------------------------------------------------------- /GUI/Misc/colorStr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | def colorStr(text: str, color): 19 | return f"{text}" 20 | -------------------------------------------------------------------------------- /GUI/MainWindow/View/__init__.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import sys 18 | 19 | from GUI.Playlist import playlistview 20 | from GUI import icons_rc 21 | 22 | sys.modules["playlistview"] = playlistview 23 | -------------------------------------------------------------------------------- /linux_build.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['main.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[('Model/Version/version.json', 'Model/Version'), ('GUI/Help/Data/get_started.md', 'GUI/Help/Data'), 9 | ('GUI/About/credits.md', 'GUI/About')], 10 | hiddenimports=['tendo'], 11 | hookspath=[], 12 | hooksconfig={}, 13 | runtime_hooks=['env_vars.py'], 14 | excludes=['PySide6'], 15 | noarchive=False, 16 | ) 17 | 18 | a.datas += Tree(r'Model/Data', prefix='Model/Data') 19 | 20 | pyz = PYZ(a.pure) 21 | 22 | exe = EXE( 23 | pyz, 24 | a.scripts, 25 | [], 26 | exclude_binaries=True, 27 | name='earquiz-frequencies', 28 | debug=False, 29 | bootloader_ignore_signals=False, 30 | strip=False, 31 | upx=True, 32 | console=False, 33 | argv_emulation=True, 34 | ) 35 | coll = COLLECT( 36 | exe, 37 | a.binaries, 38 | a.zipfiles, 39 | a.datas, 40 | strip=False, 41 | upx=True, 42 | upx_exclude=[], 43 | name='earquiz-frequencies', 44 | ) 45 | -------------------------------------------------------------------------------- /GUI/Misc/filemanager_name.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import platform 19 | 20 | 21 | def fn(): 22 | if platform.system() == 'Windows': 23 | return 'Explorer' 24 | elif platform.system() == 'Darwin': 25 | return 'Finder' 26 | else: 27 | return 'File Manager' 28 | -------------------------------------------------------------------------------- /GUI/Misc/adg_thread_run.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QRunnable, pyqtSignal, pyqtSlot, QObject 2 | import numpy as np 3 | 4 | 5 | class ADGProcSig(QObject): 6 | drillGenerated = pyqtSignal(object, np.ndarray) 7 | audioRefreshed = pyqtSignal(bool) 8 | 9 | 10 | class ADGProc(QRunnable): 11 | signals = ADGProcSig() 12 | 13 | def __init__(self, gen_func, **kwargs): 14 | super().__init__() 15 | self.gen_func = gen_func 16 | self.kwargs = kwargs 17 | 18 | @pyqtSlot(int or tuple, np.ndarray) 19 | def run(self): 20 | freq, audio = self.gen_func(**self.kwargs) 21 | self.signals.drillGenerated.emit(freq, audio) 22 | return 23 | 24 | 25 | class ADGRefresh(QRunnable): 26 | signals = ADGProcSig() 27 | 28 | def __init__(self, refresh_func, filepath=None, play_after=False): 29 | super().__init__() 30 | self.refresh_func = refresh_func 31 | self.filepath = filepath 32 | self.play_after = play_after 33 | 34 | @pyqtSlot() 35 | def run(self): 36 | self.refresh_func(filepath=self.filepath) 37 | self.signals.audioRefreshed.emit(self.play_after) 38 | -------------------------------------------------------------------------------- /Utilities/Q_extract.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | 19 | 20 | def Qextr(arg: int or float or str): 21 | if isinstance(arg, (int, float)): 22 | return arg 23 | if not isinstance(arg, str): 24 | return 25 | arg = arg.replace(' ', '') 26 | res = re.search(r'(?<=Q\=)\d+(\.\d+)?', arg) 27 | return float(res.group()) if res else None 28 | -------------------------------------------------------------------------------- /Utilities/checkMimeData.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from Utilities.urlcheck import validUrls 18 | 19 | 20 | def checkDroppedMimeData(data): 21 | if data.objectName() == 'FromPlaylist': 22 | return False 23 | if data.hasUrls(): 24 | valid_urls = validUrls(data.urls()) 25 | if valid_urls: 26 | return valid_urls 27 | return False 28 | -------------------------------------------------------------------------------- /Utilities/str2bool.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | def str2bool(value: str or bool) -> bool: 18 | if isinstance(value, bool): 19 | return value 20 | if not isinstance(value, str): 21 | raise ValueError('The value is neither string nor bool!') 22 | if value.lower() == 'true': 23 | return True 24 | if value.lower() == 'false': 25 | return False 26 | -------------------------------------------------------------------------------- /Model/globals.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from Model.AudioEngine.pinknoise_gen import generate_pinknoise 18 | 19 | supported_bitrates_mp3 = (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320) 20 | supported_bitrates_ogg = (64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500) 21 | pinknoise = generate_pinknoise() 22 | MinAudioDuration = 10 # in sec 23 | PinknoiseLength = 30 # in sec 24 | -------------------------------------------------------------------------------- /Model/file_hash.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import hashlib 18 | from functools import partial 19 | 20 | 21 | def filehash(filepath: str, buffer_size=1024 * 1024 * 50): 22 | hash_obj = hashlib.md5 23 | content = b'' 24 | with open(filepath, 'rb') as f: 25 | for chunk in iter(partial(f.read, buffer_size), b''): 26 | content = b''.join([content, chunk]) 27 | return hash_obj(content).hexdigest() 28 | -------------------------------------------------------------------------------- /Utilities/urlcheck.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from pathlib import Path 18 | 19 | 20 | def validUrls(urls: list): 21 | available_ext = ('.wav', '.aiff', '.flac', '.ogg', '.mp3', '.m3u', '.m3u8', '.pls', '.xspf') 22 | available_ext = tuple(list(available_ext) + [EXT.upper() for EXT in available_ext]) 23 | return [url for url in urls if url.toLocalFile() and Path(url.toLocalFile()).exists() 24 | and (Path(url.toLocalFile()).is_dir() or Path(url.toLocalFile()).suffix in available_ext)] 25 | -------------------------------------------------------------------------------- /GUI/Help/help_img.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | Data/Images/next-example.png 4 | Data/Images/EQ_Frequencies_Help_QRcode.png 5 | Data/Images/sequential-playback.png 6 | Data/Images/C4_A4.png 7 | Data/Images/LearnMode.png 8 | Data/Images/PreviewMode.png 9 | Data/Images/TransportPanel.png 10 | Data/Images/TestMode.png 11 | Data/Images/Frequency-Note.png 12 | Data/Images/Drill_structure.png 13 | Data/Images/BellFilterScheme.png 14 | 15 | 16 | Data/Images/youtube_16.png 17 | Data/Images/lightbulb.png 18 | Data/Images/settings_16.png 19 | Data/Images/arrows-right-and-left-filled-triangles.png 20 | Data/Images/clear.png 21 | Data/Images/media-skip-backward_16.png 22 | Data/Images/media-skip-forward_16.png 23 | 24 | 25 | Data/Images/BW_form.png 26 | Data/Images/BWoct_form.png 27 | Data/Images/Q_from_BW_form1.png 28 | Data/Images/Q_from_BW_form2.png 29 | 30 | 31 | -------------------------------------------------------------------------------- /GUI/PatternBox/patternbox_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | class PatternBoxView: 18 | def __init__(self, mw_view): 19 | self.mw_view = mw_view 20 | self.Widget = mw_view.PatternBox 21 | self.NextButton = mw_view.NextPatternBut 22 | 23 | def loadItems(self, mode_names: list[str]): 24 | for ind, N in enumerate(mode_names): 25 | self.Widget.addItem(f'{ind + 1}. {N}') 26 | 27 | def setEnabled(self, arg: bool): 28 | self.Widget.setEnabled(arg) 29 | self.NextButton.setEnabled(arg) 30 | -------------------------------------------------------------------------------- /GUI/globals.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from application import Settings 18 | 19 | default_pn_slice_length = None 20 | default_audio_slice_length = None 21 | 22 | SliderAmplitude = 2 23 | 24 | def defaultSliceLenUpd(): 25 | global default_pn_slice_length 26 | global default_audio_slice_length 27 | Settings.beginGroup('GlobalVars') 28 | default_pn_slice_length = int(Settings.value('PinknoiseSliceLength', 10)) 29 | default_audio_slice_length = int(Settings.value('ExtAudioSliceLength', 12)) 30 | Settings.endGroup() 31 | 32 | 33 | defaultSliceLenUpd() 34 | -------------------------------------------------------------------------------- /Utilities/freq2str.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | def freqString(answer: int or tuple): 18 | def hzTokHz(value: int): 19 | if value >= 1000: 20 | return f'{value / 1000}kHz' if value % 1000 != 0 else f'{int(value / 1000)}kHz' 21 | else: 22 | return f'{value}Hz' 23 | 24 | def bc(value: int): 25 | return '(+)' if value > 0 else '(-)' 26 | 27 | def valueToStr(value: int): 28 | return f'{hzTokHz(abs(value))}{bc(value)}' 29 | 30 | return valueToStr(answer) if isinstance(answer, int) else ', '.join([valueToStr(v) for v in answer]) 31 | -------------------------------------------------------------------------------- /GUI/Misc/TextBrowserDocParameters.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtGui import QTextBlockFormat 18 | 19 | 20 | def setParameters(TextBrowser, document, font_size=16, line_height=120): 21 | font = document.defaultFont() 22 | font.setPointSize(font_size) 23 | document.setDefaultFont(font) 24 | blockFmt = QTextBlockFormat() 25 | blockFmt.setLineHeight(line_height, 1) 26 | TextBrowser.selectAll() 27 | theCursor = TextBrowser.textCursor() 28 | theCursor.mergeBlockFormat(blockFmt) 29 | theCursor.clearSelection() 30 | theCursor.setPosition(0) 31 | TextBrowser.setTextCursor(theCursor) 32 | -------------------------------------------------------------------------------- /windows_build.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis( 7 | ['main.py'], 8 | pathex=[], 9 | binaries=[], 10 | datas=[('Model/Version/version.json', 'Model/Version'), ('GUI/Help/Data/get_started.md', 'GUI/Help/Data'), 11 | ('GUI/About/credits.md', 'GUI/About')], 12 | hiddenimports=['tendo'], 13 | hookspath=[], 14 | hooksconfig={}, 15 | runtime_hooks=['env_vars.py', 'pb_init.py'], 16 | excludes=['PySide6'], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher, 20 | noarchive=False, 21 | ) 22 | 23 | a.datas += Tree(r'Model/Data', prefix='Model/Data') 24 | 25 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 26 | 27 | exe = EXE( 28 | pyz, 29 | a.scripts, 30 | [], 31 | exclude_binaries=True, 32 | name='EarQuiz Frequencies', 33 | debug=False, 34 | bootloader_ignore_signals=False, 35 | strip=False, 36 | upx=True, 37 | console=False, 38 | windowed=True, 39 | disable_windowed_traceback=True, 40 | argv_emulation=True, 41 | target_arch=None, 42 | codesign_identity=None, 43 | entitlements_file=None, 44 | icon=r'GUI/Icons/Logo/EarQuiz_Icon.ico', 45 | version='version.rc', 46 | contents_directory='.' 47 | ) 48 | coll = COLLECT( 49 | exe, 50 | a.binaries, 51 | a.zipfiles, 52 | a.datas, 53 | strip=False, 54 | upx=True, 55 | upx_exclude=[], 56 | name='main', 57 | ) -------------------------------------------------------------------------------- /GUI/Misc/StartScreen.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import Qt 18 | from PyQt6.QtGui import QPixmap, QColor 19 | from PyQt6.QtWidgets import QSplashScreen 20 | from Model.get_version import version 21 | from GUI.MainWindow.View.dark_theme import blue_color 22 | 23 | 24 | class StartScreen(QSplashScreen): 25 | def __init__(self): 26 | super().__init__() 27 | logo = QPixmap(":/Logo/Icons/Logo/EarQuiz_Splash.png") 28 | self.setPixmap(logo) 29 | self.showMessage(f'v{version()}', Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop, 30 | color=QColor(f'{blue_color()}')) 31 | self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint) 32 | 33 | 34 | StartLogo = StartScreen() 35 | StartLogoTime = 1000 36 | -------------------------------------------------------------------------------- /Model/calc.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import math 18 | import random 19 | 20 | 21 | def proc_unproc_len(total_chunk_len: int or float, proc_perc=40): 22 | proc_perc = max(proc_perc, 33) 23 | proc_len = total_chunk_len * proc_perc / 100 24 | return proc_len, (total_chunk_len - proc_len) / 2 25 | 26 | 27 | def rand_buffer(): 28 | return 32 * math.pow(2, random.randint(4, 8)) 29 | 30 | 31 | def find_divider(x: int, Min=2): 32 | div = Min 33 | while x % div != 0: 34 | div += 1 35 | return div 36 | 37 | 38 | def optimal_range_length(total_length: int or float, slice_length: int or float, num_slices=10): 39 | return min(total_length // slice_length, num_slices) * slice_length 40 | 41 | 42 | def abs_tuple(value: tuple): 43 | return tuple(abs(f) for f in value) 44 | -------------------------------------------------------------------------------- /Model/get_version.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | from pathlib import Path 19 | from definitions import ROOT_DIR 20 | 21 | 22 | def _get_version_data(external_data=None): # external_data in JSON format 23 | if external_data is None: 24 | with open(Path(ROOT_DIR, 'Model', 'Version', 'version.json'), 'r', encoding='utf-8') as f: 25 | external_data = f.read() 26 | v_data = json.loads(external_data)['version'] 27 | return v_data['major'], v_data['minor'], v_data['patch'] 28 | 29 | 30 | def version(external_data=None): 31 | v_data = _get_version_data(external_data=external_data) 32 | return "%d.%d.%d" % (v_data[0], v_data[1], v_data[2]) 33 | 34 | 35 | def version_int(external_data=None): 36 | v_data = _get_version_data(external_data=external_data) 37 | return int("%02d%02d%02d" % (v_data[0], v_data[1], v_data[2])) 38 | -------------------------------------------------------------------------------- /version.rc: -------------------------------------------------------------------------------- 1 | # UTF-8 2 | # 3 | # For more details about fixed file info 'ffi' see: 4 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx 5 | VSVersionInfo( 6 | ffi=FixedFileInfo( 7 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) 8 | # Set not needed items to zero 0. 9 | filevers=(0, 1, 0, 8), 10 | prodvers=(0, 1, 0, 8), 11 | # Contains a bitmask that specifies the valid bits 'flags'r 12 | mask=0x3f, 13 | # Contains a bitmask that specifies the Boolean attributes of the file. 14 | flags=0x0, 15 | # The operating system for which this file was designed. 16 | # 0x4 - NT and there is no need to change it. 17 | OS=0x4, 18 | # The general type of file. 19 | # 0x1 - the file is an application. 20 | fileType=0x1, 21 | # The function of the file. 22 | # 0x0 - the function is not defined for this fileType 23 | subtype=0x0, 24 | # Creation date and time stamp. 25 | date=(0, 0) 26 | ), 27 | kids=[ 28 | StringFileInfo( 29 | [ 30 | StringTable( 31 | '000004b0', 32 | [StringStruct('Comments', 'This executable was built with PyInstaller.'), 33 | StringStruct('CompanyName', 'EarQuiz'), 34 | StringStruct('FileDescription', 'EarQuiz Frequencies'), 35 | StringStruct('FileVersion', '0.1.8'), 36 | StringStruct('LegalCopyright', 'Copyright © 2023-2025 by Gdaliy Garmiza'), 37 | StringStruct('OriginalFileName', 'EarQuiz Frequencies'), 38 | StringStruct('ProductName', 'EarQuiz Frequencies'), 39 | StringStruct('ProductVersion', '0.1.8')]) 40 | ]), 41 | VarFileInfo([VarStruct('Translation', [0, 1200])]) 42 | ] 43 | ) -------------------------------------------------------------------------------- /gui_compile.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import platform 19 | import subprocess 20 | 21 | from definitions import ROOT_DIR 22 | 23 | # replace with current virtual environment directory name: 24 | venv_dir = 'venv' 25 | 26 | ui_files = ('GUI/MainWindow/View/mainwindow.ui', 27 | 'GUI/AudioConvertDialog/convert_dialog_view.ui', 28 | 'GUI/MakeLearnTestFiles/make_learn_test_dialog_view.ui', 29 | 'GUI/AudioProcSettings/audio_proc_settings_widget.ui', 30 | 'GUI/About/AboutDialog.ui',) 31 | 32 | script_dir = 'bin' if platform.system() == 'Darwin' else 'Scripts' 33 | script_path = os.path.normpath(os.path.join(ROOT_DIR, venv_dir, script_dir, 'pyuic6')) 34 | 35 | for F in ui_files: 36 | full_ui_path = os.path.normpath(os.path.join(ROOT_DIR, F)) 37 | py_file = F.replace('.ui', '.py') 38 | subprocess.run([script_path, full_ui_path, '-o', py_file]) 39 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from env_vars import * 17 | import pedalboard 18 | from multiprocessing import freeze_support 19 | import multiprocessing as mp 20 | import platform 21 | import signal 22 | from tendo.singleton import SingleInstance 23 | from GUI.MainWindow.Contr.mw_contr import MainWindowContr 24 | from GUI.Misc.StartScreen import StartLogo 25 | from PyQt6.QtGui import QIcon 26 | from application import app 27 | 28 | 29 | if __name__ == '__main__': 30 | freeze_support() 31 | if platform.system() == 'Darwin': 32 | mp.set_start_method('fork') 33 | elif platform.system() == 'Linux': 34 | mp.set_start_method('spawn') 35 | me = SingleInstance() 36 | signal.signal(signal.SIGINT, signal.SIG_DFL) 37 | app.setWindowIcon(QIcon(":Logo/Icons/Logo/EarQuiz_Icon.png")) 38 | app.openFileRequest.connect(app.handle_open_file_request) 39 | StartLogo.show() 40 | app.processEvents() 41 | mw = MainWindowContr() 42 | app.exec() 43 | -------------------------------------------------------------------------------- /Utilities/selectFileInSysExplorer.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | import os 19 | import platform 20 | import subprocess 21 | import pathlib 22 | 23 | 24 | def selectFile(filepath: str): 25 | if not os.path.isfile(filepath): 26 | return 27 | if platform.system() == 'Darwin': 28 | cmd = ["open", "-R", filepath] 29 | elif platform.system() == 'Windows': 30 | filebrowser_path = os.path.join(os.getenv('WINDIR'), 'explorer.exe') 31 | filepath = rf'{filepath}' 32 | cmd = [filebrowser_path, '/select,', filepath] 33 | else: 34 | filepath = pathlib.Path(filepath).as_uri() 35 | cmd = ['dbus-send', '--session', '--print-reply', '--dest=org.freedesktop.FileManager1', '--type=method_call', 36 | '/org/freedesktop/FileManager1', 'org.freedesktop.FileManager1.ShowItems', 37 | f'array:string:{filepath}', 'string:""'] 38 | with contextlib.suppress(): 39 | subprocess.run(cmd) 40 | -------------------------------------------------------------------------------- /GUI/Misc/error_message.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6 import QtWidgets, QtCore 18 | from PyQt6.QtWidgets import QMessageBox 19 | 20 | 21 | def error_message(window, msg: str, modal='WinModal'): 22 | emsg = QtWidgets.QErrorMessage(window) 23 | if modal == 'AppModal': 24 | emsg.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) 25 | elif modal == 'WinModal': 26 | emsg.setWindowModality(QtCore.Qt.WindowModality.WindowModal) 27 | else: 28 | emsg.setWindowModality(QtCore.Qt.WindowModality.NonModal) 29 | emsg.setWindowFlag(QtCore.Qt.WindowType.WindowStaysOnTopHint) 30 | emsg.showMessage(msg) 31 | 32 | 33 | def reformat_message(window, msg='The file seems to be unreadable. Try to reformat it?'): 34 | win = QMessageBox(window) 35 | win.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) 36 | win.setDefaultButton(QMessageBox.StandardButton.Yes) 37 | win.setIcon(QMessageBox.Icon.Question) 38 | win.setText(msg) 39 | return win.exec() 40 | -------------------------------------------------------------------------------- /Model/eq_patterns.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | from pathlib import PurePath 19 | 20 | import definitions 21 | 22 | 23 | class EQPatterns: 24 | def __init__(self): 25 | with open(PurePath(definitions.ROOT_DIR, 'Model', 'Data', 'eq_patterns.json')) as f: 26 | d = json.load(f) 27 | self.List = d['Patterns'] 28 | P: dict 29 | for P in self.List: 30 | defaults = self.get_defaults(P['EQtype']) 31 | for key in defaults: 32 | P.setdefault(key, defaults[key]) 33 | 34 | def get(self, mode_num: int): # Enumeration starts from 1 35 | return self.List[mode_num - 1] 36 | 37 | @staticmethod 38 | def get_defaults(EQtype: str): 39 | return {'DualBandMode': False, 'DisableAdjacentFiltersMode': False, 'Gain_depth': 12, 40 | 'BW_Q': '1 Oct (Q=1.41)'} if EQtype == 'EQ1' \ 41 | else {'DualBandMode': False, 'DisableAdjacentFiltersMode': False, 'Gain_depth': 15, 42 | 'BW_Q': '1/3 Oct (Q=4.32)'} 43 | -------------------------------------------------------------------------------- /GUI/Modes/audiosource_modes.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | from GUI import globals as gb 19 | 20 | 21 | class PinkNoiseMode: 22 | def __init__(self, parent): # parent: MainWindowContr 23 | self.parent = parent 24 | self.name = 'Pinknoise' 25 | self.view = parent.mw_view 26 | self.parent.SRC.savePrevSourceAudioRange() 27 | with contextlib.suppress(AttributeError): 28 | self.parent.AL.setNoAudio() 29 | self.default_slice_length = gb.default_pn_slice_length 30 | self.parent.TransportContr.TransportView.SliceLenSpin.setValue(gb.default_pn_slice_length) 31 | self.parent.setMakeAudioActionsEnabled(True) 32 | self.parent.AL.load_pinknoise() 33 | 34 | 35 | class AudioFileMode: 36 | def __init__(self, parent): # parent: MainWindowContr 37 | self.parent = parent 38 | self.name = 'Audiofile' 39 | self.view = parent.mw_view 40 | with contextlib.suppress(AttributeError): 41 | self.parent.AL.setNoAudio() 42 | self.default_slice_length = gb.default_audio_slice_length 43 | self.parent.TransportContr.TransportView.SliceLenSpin.setValue(gb.default_audio_slice_length) 44 | self.view.TransportPanel.show() 45 | -------------------------------------------------------------------------------- /Model/AudioEngine/sine_wav_gen.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | from pathlib import Path 19 | 20 | import numpy as np 21 | from pedalboard.io import AudioFile 22 | 23 | from definitions import SineWaveCalibrationPath 24 | 25 | 26 | def sine_gen(freq: int, length_s=5, samplerate=44100): 27 | frames = int(length_s * samplerate) 28 | samples = np.arange(frames) / samplerate 29 | signal = np.sin(2 * np.pi * freq * samples) 30 | signal = np.float32(signal) 31 | signal /= np.max(np.abs(signal), axis=0) 32 | signal.resize((1, frames)) 33 | return signal 34 | 35 | 36 | def silence_gen(length_s=1.0, samplerate=44100): 37 | return np.zeros((1, int(samplerate * length_s))) 38 | 39 | 40 | def generateCalibrationSineTones(): 41 | sil = silence_gen() 42 | cs = np.concatenate((sil, sine_gen(1000), sil, sine_gen(10000), sil, sine_gen(100), sil, 43 | sine_gen(15000), sil, sine_gen(40)), axis=1) 44 | cs = cs * 0.2 45 | Path.mkdir(Path(SineWaveCalibrationPath).parent, parents=True, exist_ok=True) 46 | with contextlib.suppress(Exception): 47 | with AudioFile(SineWaveCalibrationPath, 'w', 44100, 1) as f: 48 | f.write(cs) 49 | return SineWaveCalibrationPath if Path(SineWaveCalibrationPath).is_file() else False 50 | -------------------------------------------------------------------------------- /GUI/AudioConvertDialog/convert_dialog_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import Qt 18 | from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QRadioButton 19 | from GUI.AudioConvertDialog.convert_dialog_view import Ui_AudioConvertDialog 20 | 21 | 22 | class ConvertFilesDialogContr(QDialog, Ui_AudioConvertDialog): 23 | def __init__(self): 24 | super().__init__() 25 | Flags = Qt.WindowType(Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | 26 | Qt.WindowType.WindowCloseButtonHint) 27 | self.setWindowFlags(Flags) 28 | self.setupUi(self) 29 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText('Convert') 30 | self.WAVBut.setChecked(True) 31 | self.SameAsOriginalBut.setChecked(True) 32 | 33 | @property 34 | def audio_format(self): 35 | return [RB.text() for RB in self.FormatGroup.findChildren(QRadioButton) if RB.isChecked()][0] 36 | 37 | @property 38 | def target_samplerate_mode(self): 39 | if self.SameAsOriginalBut.isChecked(): 40 | return 'original' 41 | if self.SR441But.isChecked(): 42 | return '44.1k' 43 | if self.SR48But.isChecked(): 44 | return '48k' 45 | if self.DivisibleBut.isChecked(): 46 | return 'auto_div' 47 | -------------------------------------------------------------------------------- /Model/AudioEngine/audio_proc_settings.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from application import Settings 18 | from Utilities.str2bool import str2bool 19 | 20 | 21 | default_EQOnTimePerc = 40 22 | default_EQTransitionDur = 35 / 1000 23 | default_ExFadeInOutDur = 5 / 1000 24 | 25 | 26 | def getEQOnTimePerc(): 27 | return int(Settings.value('AudioProcessing/EQOnTimePerc', default_EQOnTimePerc)) # in percentage 28 | 29 | 30 | def getEQTransitionDur(): 31 | return float(Settings.value('AudioProcessing/EQTransitionDur', default_EQTransitionDur)) # in seconds 32 | 33 | 34 | def getExFadeInOutDur(): 35 | return float(Settings.value('AudioProcessing/ExFadeInOutDur', default_ExFadeInOutDur)) # in seconds 36 | 37 | 38 | def getEQAlwaysOnInTest(): 39 | return str2bool(Settings.value('AudioProcessing/EQAlwaysOnInTest', False)) 40 | 41 | 42 | def setEQOnTimePerc(v: int): 43 | Settings.setValue('AudioProcessing/EQOnTimePerc', v) 44 | 45 | 46 | def setEQTransitionDur(v: int): # v: value in ms 47 | Settings.setValue('AudioProcessing/EQTransitionDur', v / 1000) 48 | 49 | 50 | def setExFadeInOutDur(v: int): # v: value in ms 51 | Settings.setValue('AudioProcessing/ExFadeInOutDur', v / 1000) # in seconds 52 | 53 | 54 | def setEQAlwaysOnInTest(v: bool): 55 | Settings.setValue('AudioProcessing/EQAlwaysOnInTest', v) 56 | -------------------------------------------------------------------------------- /GUI/FileMaker/FileCreationSuccessDialog.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | import os 19 | import pathlib 20 | import platform 21 | import subprocess 22 | from GUI.Misc.filemanager_name import fn 23 | from PyQt6.QtWidgets import QMessageBox 24 | 25 | 26 | def SuccessDialog(mw, filespath: str, mode_name='Learning'): 27 | msg = QMessageBox(mw) 28 | msg.setText(f'{mode_name} files successfully created in "{filespath}"!') 29 | msg.setStandardButtons(QMessageBox.StandardButton.Open | QMessageBox.StandardButton.Close) 30 | msg.button(QMessageBox.StandardButton.Open).setText(f'Show in {fn()}') 31 | btn = msg.exec() 32 | if btn == QMessageBox.StandardButton.Open: 33 | if platform.system() == 'Darwin': 34 | with contextlib.suppress(Exception): 35 | subprocess.run(["open", filespath]) 36 | elif platform.system() == 'Windows': 37 | os.startfile(filespath) 38 | elif platform.system() == 'Linux': 39 | filespath = pathlib.Path(filespath).as_uri() 40 | subprocess.run(['dbus-send', '--session', '--print-reply', '--dest=org.freedesktop.FileManager1', 41 | '--type=method_call', '/org/freedesktop/FileManager1', 42 | 'org.freedesktop.FileManager1.ShowFolders', 43 | f'array:string:{filespath}', 'string:""']) 44 | -------------------------------------------------------------------------------- /Model/export_playlist.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | from pathlib import Path 19 | from GUI.Playlist.plsong import PlSong 20 | 21 | 22 | def export_m3u_playlist(playlistdata: list[PlSong], out_fullpath: str, pathmode='absolute', ext='.m3u', encoding=None): 23 | encoding = 'utf-8' if ext == '.m3u8' else encoding 24 | out_fullpath = f'{out_fullpath}{ext}' if Path(out_fullpath).suffix != ext else out_fullpath 25 | out_dir = Path(out_fullpath).parent 26 | pl_paths = playlist_paths(playlistdata, out_dir=out_dir, pathmode=pathmode) 27 | pl_paths_str = '\n'.join(pl_paths) 28 | Path.mkdir(out_dir, parents=True, exist_ok=True) 29 | with open(out_fullpath, "w", encoding=encoding, errors='replace') as f: 30 | f.write(pl_paths_str) 31 | return out_fullpath if Path(out_fullpath).is_file() else False 32 | 33 | 34 | def playlist_paths(playlistdata: list[PlSong], out_dir=None, pathmode='absolute'): 35 | if pathmode == 'absolute' or out_dir is None: 36 | pl_paths = [str(Path(P.path).absolute()) for P in playlistdata] 37 | else: 38 | pl_paths = [] 39 | for P in playlistdata: 40 | _path = str(Path(P.path).absolute()) 41 | with contextlib.suppress(ValueError): 42 | _path = str(Path(P.path).absolute().relative_to(out_dir)) 43 | pl_paths.append(_path) 44 | return pl_paths 45 | -------------------------------------------------------------------------------- /definitions.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import platform 19 | 20 | ROOT_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))) 21 | DATA_DIR = '' 22 | if platform.system() == 'Darwin': 23 | DATA_DIR = os.path.expanduser('~/Library/Application Support/EarQuiz/Frequencies') 24 | elif platform.system() == 'Windows': 25 | DATA_DIR = os.path.normpath(os.path.join(os.path.expandvars('%AppData%'), 'EarQuiz', 'Frequencies')) 26 | elif platform.system() == 'Linux': 27 | DATA_DIR = os.path.expanduser('~/.config/EarQuiz/Frequencies') 28 | SETTINGS_PATH = os.path.normpath(os.path.join(DATA_DIR, 'config.ini')) 29 | TEMP_AUDIO_DIR = os.path.normpath(os.path.join(DATA_DIR, 'temp_audio')) 30 | CURRENT_PLAYLIST_PATH = os.path.normpath(os.path.join(DATA_DIR, 'Playlists', 'current.m3u8')) 31 | os.makedirs(DATA_DIR, exist_ok=True) 32 | USER_DOCS_DIR = os.path.normpath(os.path.expanduser('~/Documents/EarQuiz Frequencies')) 33 | EXERCISE_DIR = os.path.normpath(os.path.join(USER_DOCS_DIR, 'Exercises')) 34 | PLAYLIST_DIR = os.path.normpath(os.path.join(USER_DOCS_DIR, 'Playlists')) 35 | 36 | SineWaveCalibrationFilename = '1kHz__10kHz__100Hz__15kHz__40Hz Sinus Tones.wav' 37 | SineWaveCalibrationPath = os.path.normpath(os.path.join(DATA_DIR, 'Audio', SineWaveCalibrationFilename)) 38 | 39 | SourceRangeLib_DIR = os.path.normpath(os.path.join(DATA_DIR, 'SourceRangeLib')) 40 | 41 | PN = 'Pink noise' 42 | -------------------------------------------------------------------------------- /Model/sourcerange_manager.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | import os 19 | from pathlib import Path 20 | from Model.AudioEngine.preview_audio import PreviewAudioCrop 21 | from definitions import SourceRangeLib_DIR 22 | 23 | 24 | class SourceRangeManager: 25 | def __init__(self): 26 | pass 27 | 28 | def save(self, filehash: str, filename: str, SourceRange: PreviewAudioCrop): 29 | out_path = Path(SourceRangeLib_DIR, self._filename(filehash)).absolute() 30 | os.makedirs(SourceRangeLib_DIR, exist_ok=True) 31 | content = {'Audiofile': filename, 'Range': (SourceRange.starttime, SourceRange.endtime), 32 | 'SliceLength': SourceRange.slice_length} 33 | with open(out_path, 'w', encoding='utf-8', errors='replace') as f: 34 | f.write(json.dumps(content, indent=1)) 35 | 36 | def get(self, filehash: str): 37 | filename = self._filename(filehash) 38 | filepath = Path(SourceRangeLib_DIR, filename) 39 | if not filepath.is_file(): 40 | return None 41 | filepath = filepath.absolute() 42 | with open(filepath, encoding='utf-8', errors='replace') as f: 43 | content = json.loads(f.read()) 44 | try: 45 | return *content['Range'], content['SliceLength'] 46 | except KeyError: 47 | return None 48 | 49 | @staticmethod 50 | def _filename(filehash: str): 51 | return f'{filehash}.afab' 52 | -------------------------------------------------------------------------------- /macos_build.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis( 8 | ['main.py'], 9 | pathex=[], 10 | binaries=[], 11 | datas=[('Model/Version/version.json', 'Model/Version'), ('GUI/Help/Data/get_started.md', 'GUI/Help/Data'), 12 | ('GUI/About/credits.md', 'GUI/About')], 13 | hiddenimports=['tendo'], 14 | hookspath=[], 15 | runtime_hooks=['env_vars.py'], 16 | excludes=['PySide6'], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher, 20 | noarchive=False, 21 | ) 22 | 23 | a.datas += Tree('Model/Data', prefix='Model/Data') 24 | 25 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 26 | 27 | exe = EXE( 28 | pyz, 29 | a.scripts, 30 | exclude_binaries=True, 31 | name='eqfreq', 32 | debug=False, 33 | bootloader_ignore_signals=False, 34 | strip=False, 35 | upx=False, 36 | console=False, 37 | argv_emulation=False, 38 | target_arch='x86_64', 39 | codesign_identity=None, 40 | entitlements_file=None, 41 | ) 42 | coll = COLLECT( 43 | exe, 44 | a.binaries, 45 | a.zipfiles, 46 | a.datas, 47 | strip=False, 48 | upx=False, 49 | upx_exclude=[], 50 | name='eqfreq', 51 | ) 52 | app = BUNDLE( 53 | coll, 54 | name='EarQuiz Frequencies.app', 55 | icon='GUI/Icons/Logo/EarQuiz_Icon.icns', 56 | info_plist={ 57 | 'CFBundleDisplayName': 'EarQuiz Frequencies', 58 | 'CFBundleExecutable': 'eqfreq', 59 | 'CFBundleInfoDictionaryVersion': '6.0', 60 | 'CFBundleName': 'EarQuiz Frequencies', 61 | 'CFBundlePackageType': 'APPL', 62 | 'CFBundleSignature': 'EQFREQ', 63 | 'CFBundleIdentifier': 'org.earquiz.frequencies', 64 | 'CFBundleShortVersionString': '0.1.8', 65 | 'NSHighResolutionCapable': True, 66 | 'LSMinimumSystemVersion': '12.0.0', 67 | 'CFBundleDevelopmentRegion': 'en_US', 68 | 'CFBundleDocumentTypes': [{ 69 | 'CFBundleTypeName': 'EarQuiz_Frequencies_Audio', 70 | 'CFBundleTypeExtensions': ['wav', 'flac', 'mp3', 'ogg', 'aiff', 'pls', 'xspf', 'm3u', 'm3u8',], 71 | 'CFBundleTypeRole': "Viewer", 72 | }], 73 | 'NSHumanReadableCopyright': '© 2023-2025 Gdaliy Garmiza', 74 | 'NSPrincipalClass': 'NSApplication' 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /macos_build-arm64.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis( 8 | ['main.py'], 9 | pathex=[], 10 | binaries=[], 11 | datas=[('Model/Version/version.json', 'Model/Version'), ('GUI/Help/Data/get_started.md', 'GUI/Help/Data'), 12 | ('GUI/About/credits.md', 'GUI/About')], 13 | hiddenimports=['tendo'], 14 | hookspath=[], 15 | runtime_hooks=['env_vars.py'], 16 | excludes=['PySide6'], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher, 20 | noarchive=False, 21 | ) 22 | 23 | a.datas += Tree('Model/Data', prefix='Model/Data') 24 | 25 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 26 | 27 | exe = EXE( 28 | pyz, 29 | a.scripts, 30 | exclude_binaries=True, 31 | name='eqfreq', 32 | debug=False, 33 | bootloader_ignore_signals=False, 34 | strip=False, 35 | upx=False, 36 | console=False, 37 | argv_emulation=False, 38 | target_arch='arm64', 39 | codesign_identity=None, 40 | entitlements_file=None, 41 | ) 42 | coll = COLLECT( 43 | exe, 44 | a.binaries, 45 | a.zipfiles, 46 | a.datas, 47 | strip=False, 48 | upx=False, 49 | upx_exclude=[], 50 | name='eqfreq', 51 | ) 52 | app = BUNDLE( 53 | coll, 54 | name='EarQuiz Frequencies.app', 55 | icon='GUI/Icons/Logo/EarQuiz_Icon.icns', 56 | info_plist={ 57 | 'CFBundleDisplayName': 'EarQuiz Frequencies', 58 | 'CFBundleExecutable': 'eqfreq', 59 | 'CFBundleInfoDictionaryVersion': '6.0', 60 | 'CFBundleName': 'EarQuiz Frequencies', 61 | 'CFBundlePackageType': 'APPL', 62 | 'CFBundleSignature': 'EQFREQ', 63 | 'CFBundleIdentifier': 'org.earquiz.frequencies', 64 | 'CFBundleShortVersionString': '0.1.8', 65 | 'NSHighResolutionCapable': True, 66 | 'LSMinimumSystemVersion': '13.0.0', 67 | 'CFBundleDevelopmentRegion': 'en_US', 68 | 'CFBundleDocumentTypes': [{ 69 | 'CFBundleTypeName': 'EarQuiz_Frequencies_Audio', 70 | 'CFBundleTypeExtensions': ['wav', 'flac', 'mp3', 'ogg', 'aiff', 'pls', 'xspf', 'm3u', 'm3u8',], 71 | 'CFBundleTypeRole': "Viewer", 72 | }], 73 | 'NSHumanReadableCopyright': '© 2023-2025 Gdaliy Garmiza', 74 | 'NSPrincipalClass': 'NSApplication' 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /GUI/EQSettings/eqset_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | class EQSetView: 18 | def __init__(self, mw_view): 19 | self.mw_view = mw_view 20 | self.GainRangeSpin = self.mw_view.GainRangeSpin 21 | self.BWBox = self.mw_view.BWBox 22 | self.ResetBut = self.mw_view.ResetEQBut 23 | self.LockEQSettingsBut = self.mw_view.LockEQSettingsBut 24 | self.mw_view.LockEQSettingsBut.setDefaultAction(self.mw_view.actionLockEQSettings) 25 | self.mw_view.GainRangeSpin.valueChanged.connect(self.mw_view.status.FreqGainLabel.update) 26 | 27 | def refreshBWQList(self, items: list[str]): 28 | self.BWBox.clear() 29 | for item in items: 30 | self.BWBox.addItem(item) 31 | 32 | def update(self, gain_depth: int, BW: str): 33 | self.update_gain_depth(gain_depth) 34 | self.update_BW(BW) 35 | 36 | def update_gain_depth(self, gain_depth: int): 37 | self.GainRangeSpin.blockSignals(True) 38 | self.GainRangeSpin.setValue(gain_depth) 39 | self.GainRangeSpin.blockSignals(False) 40 | self.mw_view.status.FreqGainLabel.update(gain_depth) 41 | 42 | def update_BW(self, BW: str): 43 | self.BWBox.blockSignals(True) 44 | self.BWBox.setCurrentText(BW) 45 | self.BWBox.blockSignals(False) 46 | 47 | def setEnabled(self, arg: bool): 48 | self.BWBox.setEnabled(arg) 49 | self.GainRangeSpin.setEnabled(arg) 50 | self.ResetBut.setEnabled(arg) 51 | self.LockEQSettingsBut.setEnabled(arg) 52 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\Python312-x64" 4 | PYTHON_VERSION: "3.12.8" 5 | PYTHON_ARCH: "64" 6 | ISS_FILE_PATH: '"C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"' 7 | 8 | platform: x64 9 | 10 | configuration: Release 11 | 12 | # the first failed job cancels other jobs and fails entire build 13 | matrix: 14 | fast_finish: true 15 | 16 | # Not a project with an msbuild file, build done at install. 17 | build: None 18 | 19 | init: 20 | - cmd: ver 21 | - cmd: ECHO Processor architecture - %PROCESSOR_ARCHITECTURE% 22 | - cmd: wmic OS get OSArchitecture 23 | 24 | # As AppVeyor has multiple python install, check which one uses by default 25 | - cmd: ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH% 26 | - cmd: python --version 27 | - cmd: python -c "import struct; print(struct.calcsize('P') * 8)" 28 | - cmd: python -c "import sys; print(sys.executable)" 29 | 30 | # Set the relevant Python and pip location to the path 31 | - cmd: set PATH=%PYTHON%;%PYTHON%\scripts;%PATH% 32 | - cmd: ECHO Path - %PATH% 33 | 34 | # Verify the new default python 35 | - cmd: python --version 36 | - cmd: python -c "import struct; print(struct.calcsize('P') * 8)" 37 | - cmd: python -c "import sys; print(sys.executable)" 38 | - cmd: pip3 --version 39 | - ps: rm -r $env:LOCALAPPDATA\pip\cache\selfcheck\ 40 | - ps: python.exe -m pip install --upgrade pip 41 | 42 | # Check out installed python packages 43 | - cmd: pip3 freeze 44 | 45 | install: 46 | - ps: pip3 install -r requirements.txt 47 | - cmd: pip3 install -U pyinstaller 48 | - cmd: pyinstaller windows_build.spec 49 | 50 | # Remove unnecessary items 51 | - cmd: rmdir /s /q dist\main\pyqtgraph\icons 52 | 53 | # Make installer file 54 | - cmd: '%ISS_FILE_PATH% Distribution\Windows\eqfreq_AppVeyor.iss' 55 | 56 | artifacts: 57 | - path: Distribution\Windows\output\*.exe 58 | 59 | version: 0.1.8-build-{build} 60 | pull_requests: 61 | do_not_increment_build_number: true 62 | 63 | deploy: 64 | - provider: Webhook 65 | url: https://app.signpath.io/API/v1/5d80b854-5feb-4128-ac4c-1ef9a083c0b1/Integrations/AppVeyor?ProjectSlug=EarQuiz_Frequencies&SigningPolicySlug=release-signing&ArtifactConfigurationSlug=initial 66 | authorization: 67 | secure: B6b9qk+BJzltehPQLaWiOP8hu5cQEbuHWCkXGFC4xetttaB0dXSbRD+dGt1UTl7LTfJXlIhB9S0FKKjwOLF+Rg== 68 | -------------------------------------------------------------------------------- /GUI/AudioProcSettings/eq_indicator_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import Qt, QObject, QRect 18 | from PyQt6.QtWidgets import QLabel 19 | from PyQt6.QtGui import QPainter, QPixmap, QColor 20 | from GUI.MainWindow.View.dark_theme import green_color 21 | from Utilities.common_calcs import eq_off_perc 22 | import Model.AudioEngine.audio_proc_settings as aps 23 | import platform 24 | 25 | 26 | class EqOnOffIndicatorView(QObject): 27 | def __init__(self, IndicatorLabel: QLabel, EqOnPerc=aps.getEQOnTimePerc()): 28 | super().__init__() 29 | self.IndLab = IndicatorLabel 30 | if platform.system() != 'Darwin': 31 | self.IndLab.setMinimumWidth(385) 32 | self.EqOnPerc = EqOnPerc 33 | self.IndLab.setScaledContents(True) 34 | self.update(self.EqOnPerc) 35 | 36 | @property 37 | def w(self) -> int: 38 | return self.IndLab.width() 39 | 40 | @property 41 | def h(self) -> int: 42 | return self.IndLab.height() 43 | 44 | def update(self, EqOnPerc=aps.getEQOnTimePerc()): 45 | self.EqOnPerc = EqOnPerc 46 | canvas = QPixmap(self.w, self.h) 47 | canvas.fill(Qt.GlobalColor.gray) 48 | color = QColor(green_color()) 49 | painter = QPainter(canvas) 50 | painter.setBrush(color) 51 | painter.drawRect(self.eqOnRect(EqOnPerc)) 52 | painter.end() 53 | self.IndLab.setPixmap(canvas) 54 | 55 | def eqOnRect(self, EqOnPerc: int or float) -> QRect: 56 | return QRect(int(self.w * eq_off_perc(EqOnPerc) / 100), -5, int(self.w * EqOnPerc / 100) - 1, self.h + 5) 57 | -------------------------------------------------------------------------------- /GUI/About/about_dialog_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import platform 18 | from pathlib import Path 19 | from definitions import ROOT_DIR 20 | from GUI.About.AboutDialog import Ui_AboutDialog 21 | from GUI.Misc.TextBrowserDocParameters import setParameters 22 | from PyQt6.QtWidgets import QDialog, QLabel 23 | from PyQt6.QtGui import QTextDocument 24 | from Model.get_version import version 25 | 26 | 27 | class AboutDialogView(QDialog, Ui_AboutDialog): 28 | def __init__(self): 29 | super().__init__() 30 | self.setupUi(self) 31 | self.win_linux_os_settings() 32 | self.setCredits() 33 | self.VersionLab.setText(f'Version {version()}') 34 | self.tabWidget.setCurrentIndex(0) 35 | 36 | def win_linux_os_settings(self): 37 | if platform.system() == 'Darwin': 38 | return 39 | labels = self.findChildren(QLabel) 40 | L: QLabel 41 | for L in labels: 42 | font = L.font() 43 | size = font.pointSize() - 2 44 | font.setPointSize(size) 45 | L.setFont(font) 46 | 47 | def setCredits(self): 48 | content_path = Path(ROOT_DIR, 'GUI', 'About', 'credits.md').absolute() 49 | with open(content_path, 'r', encoding='utf-8') as f: 50 | content = f.read() 51 | document = QTextDocument() 52 | document.setMarkdown(content) 53 | self.creditsText.setDocument(document) 54 | if platform.system() == 'Darwin': 55 | font_size = 14 56 | line_height = 120 57 | else: 58 | font_size = 12 59 | line_height = 110 60 | setParameters(self.creditsText, document, font_size=font_size, line_height=line_height) 61 | -------------------------------------------------------------------------------- /GUI/FileMaker/make_playlist.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | from pathlib import Path 19 | 20 | from PyQt6.QtWidgets import QFileDialog 21 | 22 | from GUI.Playlist.plsong import PlSong 23 | from Model.export_playlist import export_m3u_playlist 24 | from definitions import PLAYLIST_DIR, CURRENT_PLAYLIST_PATH 25 | 26 | 27 | def exportPlaylistWithRelPaths(mw, playlistdata: list[PlSong]): 28 | return exportPlaylist(mw, playlistdata, pathmode='relative') 29 | 30 | 31 | def exportPlaylist(mw, playlistdata: list[PlSong], pathmode='absolute'): 32 | Path(PLAYLIST_DIR).mkdir(parents=True, exist_ok=True) 33 | FileDialog = QFileDialog(mw) 34 | m3u_mask = 'M3U (*.m3u)' 35 | m3u8_mask = 'M3U8 (*.m3u8)' 36 | formats = f'{m3u_mask};;{m3u8_mask}' 37 | filename, _format = FileDialog.getSaveFileName(mw, 'Export Playlist As...', 38 | PLAYLIST_DIR, filter=formats, initialFilter=m3u8_mask) 39 | result = False 40 | if filename and _format in (m3u_mask, m3u8_mask): 41 | ext = '.m3u8' if _format == m3u8_mask else '.m3u' 42 | enc = 'utf-8' if _format == m3u8_mask else None 43 | try: 44 | result = export_m3u_playlist(playlistdata, filename, pathmode=pathmode, ext=ext, encoding=enc) 45 | except Exception as e: 46 | mw.error_msg(f'Error exporting playlist! {str(e)}') 47 | return result 48 | 49 | 50 | def saveCurrentPlaylist(playlistdata: list[PlSong]): 51 | Path(Path(CURRENT_PLAYLIST_PATH).parent).mkdir(parents=True, exist_ok=True) 52 | with contextlib.suppress(Exception): 53 | export_m3u_playlist(playlistdata, CURRENT_PLAYLIST_PATH, pathmode='absolute', ext='.m3u8', encoding='utf-8') 54 | -------------------------------------------------------------------------------- /GUI/MainWindow/Contr/learn_freq_order_handler.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import QObject 18 | from PyQt6.QtGui import QActionGroup 19 | 20 | 21 | class LearnFreqOrderHandler(QObject): 22 | LearnFreqOrderActionGroup: QActionGroup 23 | BoostCutOrderActionGroup: QActionGroup 24 | 25 | def __init__(self, parent): 26 | super().__init__() 27 | self.mw_contr = parent 28 | self.mw_view = parent.mw_view 29 | self.setLearnFreqOrderAG() 30 | self.setBoostCutOrderAG() 31 | 32 | def setLearnFreqOrderAG(self): 33 | self.LearnFreqOrderActionGroup = QActionGroup(self) 34 | self.LearnFreqOrderActionGroup.addAction(self.mw_view.actionAscendingEQ) 35 | self.LearnFreqOrderActionGroup.addAction(self.mw_view.actionDescendingEQ) 36 | self.LearnFreqOrderActionGroup.addAction(self.mw_view.actionShuffleEQ) 37 | self.LearnFreqOrderActionGroup.triggered.connect(self.onLearnFreqOrderActionChanged) 38 | 39 | def setBoostCutOrderAG(self): 40 | self.BoostCutOrderActionGroup = QActionGroup(self) 41 | self.BoostCutOrderActionGroup.addAction(self.mw_view.actionEach_Band_Boosted_then_Cut) 42 | self.BoostCutOrderActionGroup.addAction(self.mw_view.actionAll_Bands_Boosted_then_All_Bands_Cut) 43 | self.BoostCutOrderActionGroup.triggered.connect(self.onBoostCutOrderActionChanged) 44 | 45 | def onLearnFreqOrderActionChanged(self): 46 | if self.mw_contr.ADGen is None: 47 | return 48 | self.mw_contr.ADGen.order = self.mw_contr.freqOrder() 49 | 50 | def onBoostCutOrderActionChanged(self): 51 | if self.mw_contr.ADGen is None: 52 | return 53 | self.mw_contr.ADGen.boost_cut_priority = self.mw_contr.boostCutPriority 54 | -------------------------------------------------------------------------------- /GUI/TransportPanel/cropregiontimestr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import QTime 18 | from Utilities.common_calcs import hhmmss, get_sec 19 | 20 | 21 | class CropRegionTimestr: 22 | def __init__(self, parent): # parent: TransportView 23 | self.parent = parent 24 | self.StartTimeEdit = parent.StartTimeEdit 25 | self.EndTimeEdit = parent.EndTimeEdit 26 | 27 | def setValues(self, starttime_s: int or float, endtime_s: int or float): 28 | starttime = QTime(*hhmmss(starttime_s, string=False)) 29 | endtime = QTime(*hhmmss(endtime_s, string=False)) 30 | self.StartTimeEdit.blockSignals(True) 31 | self.StartTimeEdit.setTime(starttime) 32 | self.StartTimeEdit.blockSignals(False) 33 | self.EndTimeEdit.blockSignals(True) 34 | self.EndTimeEdit.setTime(endtime) 35 | self.EndTimeEdit.blockSignals(False) 36 | 37 | def getValues(self): 38 | return get_sec(self.StartTimeEdit.time().toString('HH:mm:ss.zzz')), \ 39 | get_sec(self.EndTimeEdit.time().toString('HH:mm:ss.zzz')) 40 | 41 | def noAudioState(self, arg: bool): 42 | if arg: 43 | self.setValues(0, 0) 44 | self.setChangesEnabled(not arg) 45 | 46 | def setReadOnly(self, arg: bool): 47 | self.StartTimeEdit.setReadOnly(arg) 48 | self.EndTimeEdit.setReadOnly(arg) 49 | self.StartTimeEdit.setEnabled(not arg) 50 | self.EndTimeEdit.setEnabled(not arg) 51 | 52 | def setChangesEnabled(self, arg: bool): 53 | self.parent.StartPointBut.setEnabled(arg) 54 | self.parent.EndPointBut.setEnabled(arg) 55 | self.parent.RangeToStart.setEnabled(arg) 56 | self.parent.RangeToEnd.setEnabled(arg) 57 | self.parent.ClearRangeBut.setEnabled(arg) 58 | self.setReadOnly(not arg) 59 | -------------------------------------------------------------------------------- /Utilities/common_calcs.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import math 18 | from fractions import Fraction 19 | 20 | 21 | def findAdjacentEl(L: list, element, 22 | num=1): # num: the maximum number of adjacent elements from each side of given element 23 | element_ind = L.index(element) 24 | min_ind = max(0, element_ind - num) 25 | max_ind = min(len(L) - 1, element_ind + num) 26 | return [L[i] for i in range(min_ind, max_ind + 1) if L[i] != element] 27 | 28 | 29 | def Qcalc(BW_Noct: float or int or str): 30 | N = float(Fraction(BW_Noct)) if isinstance(BW_Noct, str) else BW_Noct 31 | return round(math.sqrt(2 ** N) / (2 ** N - 1), 2) 32 | 33 | 34 | def mmss(s, string=False): 35 | m, s = divmod(s, 60) 36 | return ['%02d' % m, '%02d' % s] if string else (m, s) 37 | 38 | 39 | def hhmmss(secs, string=True): 40 | ms = secs * 1000 41 | secs, ms = divmod(ms, 1000) 42 | mins, secs = divmod(secs, 60) 43 | hours, mins = divmod(mins, 60) 44 | return '%02d:%02d:%02d.%03d' % (hours, mins, secs, ms) if string else (int(hours), int(mins), int(secs), int(ms)) 45 | 46 | 47 | def get_sec(time_str): 48 | h, m, s = time_str.split(':') 49 | return int(h) * 3600 + int(m) * 60 + float(s) 50 | 51 | 52 | def ms2samp(ms: int or float, samplerate=44100): 53 | return ms * samplerate / 1000 54 | 55 | 56 | def samp2ms(samples: int, samplerate=44100): 57 | return samples / samplerate * 1000 58 | 59 | 60 | def round_s(secs: int or float): 61 | return round(secs * 1000) / 1000 62 | 63 | 64 | def eq_off_perc(eq_on_perc: int or float): 65 | result = (100 - eq_on_perc) / 2 66 | return int(result) if result == int(result) else result 67 | 68 | 69 | def perc2sec(full_length_sec: int or float, percent: int or float): 70 | result = round(full_length_sec * percent / 100, 3) 71 | return int(result) if result == int(result) else result 72 | -------------------------------------------------------------------------------- /application.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import platform 17 | import sys 18 | import copy 19 | from PyQt6 import QtCore, QtWidgets 20 | from PyQt6.QtCore import QSettings, QLibraryInfo 21 | from PyQt6.QtMultimedia import QMediaDevices 22 | from PyQt6.QtGui import QFont 23 | from definitions import SETTINGS_PATH 24 | 25 | app_name = 'EarQuiz Frequencies' 26 | launch_files_onstart = sys.argv[1:] if len(sys.argv) > 1 else None 27 | IsWin11 = platform.system() == 'Windows' and sys.getwindowsversion().build >= 22000 28 | 29 | 30 | class EQFreqApp(QtWidgets.QApplication): 31 | openFileRequest = QtCore.pyqtSignal(QtCore.QUrl, name='openFileRequest') 32 | 33 | def __init__(self, *args, **kwargs): 34 | super().__init__(*args, **kwargs) 35 | self.setApplicationDisplayName(app_name) 36 | self.setApplicationName(app_name) 37 | if platform.system() != 'Linux': 38 | self.setOrganizationDomain("earquiz.org") 39 | if IsWin11: 40 | self.setStyle('Fusion') 41 | self.setOrganizationName("EarQuiz") 42 | self.set_app_font() 43 | self.files_to_be_opened = copy.copy(launch_files_onstart) 44 | 45 | def event(self, event): 46 | if event.type() == QtCore.QEvent.Type.FileOpen: 47 | self.openFileRequest.emit(event.url()) 48 | return True 49 | return super().event(event) 50 | 51 | def set_app_font(self): 52 | if platform.system() == 'Darwin': 53 | self.setFont(QFont('Arial', 13)) 54 | else: 55 | self.setFont(QFont('Arial', 11)) 56 | 57 | def handle_open_file_request(self, url): 58 | if self.files_to_be_opened is None: 59 | self.files_to_be_opened = [] 60 | self.files_to_be_opened.append(url.toLocalFile()) 61 | 62 | 63 | app = EQFreqApp(list(sys.argv)) 64 | MediaDevices = QMediaDevices() 65 | Settings = QSettings(SETTINGS_PATH, QSettings.Format.IniFormat) 66 | QtVersion = QLibraryInfo.version().toString() 67 | -------------------------------------------------------------------------------- /GUI/TransportPanel/player_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import math 18 | from PyQt6.QtCore import QSize 19 | from PyQt6.QtGui import QIcon 20 | 21 | 22 | class PlayerView: 23 | def __init__(self, mw_view): 24 | self.mw_view = mw_view 25 | self.setPlayerButtons() 26 | 27 | def upd_VolumeLevelLab(self, value: float): 28 | try: 29 | level_db = round(20 * math.log10(value), 1) 30 | except ValueError: 31 | level_db = '-∞' 32 | 33 | self.mw_view.VolumeLevelLab.setText(f'{self.mw_view.VolumeSlider.value()}% ({level_db}dB)') 34 | 35 | def setPlayerButtons(self): 36 | self.mw_view.Player_PlayPause.setDefaultAction(self.mw_view.actionPlayPause) 37 | self.mw_view.Player_Stop.setDefaultAction(self.mw_view.actionStop) 38 | self.mw_view.Player_SkipBackw.setDefaultAction(self.mw_view.actionPrevious_Track) 39 | self.mw_view.Player_SkipForw.setDefaultAction(self.mw_view.actionNext_Track) 40 | self.mw_view.LoopButton.setDefaultAction(self.mw_view.actionLoop_Playback) 41 | self.mw_view.SequencePlayBut.setDefaultAction(self.mw_view.actionSequential_Playback) 42 | self.setPlayPause2Play() 43 | 44 | def setPlayPause2Play(self): 45 | self.mw_view.actionPlayPause.setText("Play") 46 | icon = QIcon() 47 | icon.addFile(u":Player/Icons/Player/Actions-media-playback-start-icon.png", QSize(32, 32), 48 | QIcon.Mode.Normal, QIcon.State.Off) 49 | self.mw_view.actionPlayPause.setIcon(icon) 50 | 51 | def setPlayPause2Pause(self): 52 | self.mw_view.actionPlayPause.setText("Pause") 53 | icon = QIcon() 54 | icon.addFile(u":Player/Icons/Player/Actions-media-playback-pause-icon.png", QSize(32, 32), 55 | QIcon.Mode.Normal, QIcon.State.Off) 56 | self.mw_view.actionPlayPause.setIcon(icon) 57 | 58 | @staticmethod 59 | def pb_state2str(state): 60 | return str(state).split('.')[1].replace('State', '') 61 | -------------------------------------------------------------------------------- /GUI/UpdateChecker/update_checker_runner.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import QRunnable, QObject, pyqtSignal, pyqtSlot 18 | import json 19 | import certifi 20 | from urllib import request 21 | from Model.get_version import version_int 22 | 23 | 24 | class UpdRunSignals(QObject): 25 | finished = pyqtSignal() 26 | error = pyqtSignal(str) 27 | 28 | 29 | class UpdCheckRun(QRunnable): 30 | VersionData_URL = 'https://www.dropbox.com/s/gha3fm6duhh87so/version.json?dl=1' 31 | signals = UpdRunSignals() 32 | cert = certifi.where() 33 | 34 | def __init__(self): 35 | super().__init__() 36 | self.upd_data = None 37 | self.in_process = False 38 | 39 | @pyqtSlot() 40 | def run(self): 41 | self.in_process = True 42 | try: 43 | upd_data = request.urlopen(self.VersionData_URL, cafile=self.cert).read() 44 | latest_version = version_int(external_data=upd_data) 45 | except Exception as e: 46 | self.signals.error.emit(str(e)) 47 | return 48 | 49 | curr_version = version_int() 50 | if latest_version <= curr_version: 51 | self._no_upd() 52 | return 53 | self.upd_data = json.loads(upd_data) 54 | if not self.upd_data['update_active']: 55 | self._no_upd(value='no_active_upd') 56 | return 57 | elif self.checkVersionInfo(): 58 | self.signals.finished.emit() 59 | else: 60 | self.signals.error.emit('Could not load the new version info!') 61 | 62 | def checkVersionInfo(self): 63 | info_url = self.upd_data['info_file'] 64 | if not info_url: 65 | return True 66 | try: 67 | info_data = request.urlopen(info_url, cafile=self.cert).read() 68 | self.upd_data['info_data'] = info_data.decode('utf-8') 69 | except Exception as e: 70 | self.signals.error.emit(str(e)) 71 | return False 72 | return True 73 | 74 | def _no_upd(self, value='no_upd'): 75 | self.upd_data = value 76 | self.signals.finished.emit() 77 | -------------------------------------------------------------------------------- /Distribution/Windows/eqfreq_AppVeyor.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "EarQuiz Frequencies" 5 | #define MyAppVersion=GetEnv('APPVEYOR_BUILD_VERSION') 6 | #define MyAppPublisher "EarQuiz" 7 | #define MyAppURL "https://earquiz.org/EQ_Frequencies/" 8 | #define MyAppExeName "EarQuiz Frequencies.exe" 9 | #define AppRepPath=GetEnv('APPVEYOR_BUILD_FOLDER') 10 | 11 | [Setup] 12 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 13 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 14 | AppId={#MyAppName} 15 | AppName={#MyAppName} 16 | AppVersion={#MyAppVersion} 17 | AppCopyright=Copyright (C) 2023-2025 by Gdaliy Garmiza 18 | VersionInfoVersion=0.1.0.8 19 | ;AppVerName={#MyAppName} {#MyAppVersion} 20 | AppPublisher={#MyAppPublisher} 21 | AppPublisherURL={#MyAppURL} 22 | AppSupportURL={#MyAppURL} 23 | AppUpdatesURL={#MyAppURL} 24 | DefaultDirName={autopf}\EarQuiz\Frequencies 25 | DisableProgramGroupPage=yes 26 | LicenseFile={#AppRepPath}\Distribution\Windows\LICENSE.txt 27 | InfoBeforeFile={#AppRepPath}\Distribution\Windows\README.txt 28 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 29 | PrivilegesRequired=lowest 30 | OutputDir={#AppRepPath}\Distribution\Windows\output 31 | OutputBaseFilename=eqfreq_v{#MyAppVersion} 32 | SetupIconFile={#AppRepPath}\GUI\Icons\Logo\EarQuiz_Icon.ico 33 | UninstallDisplayIcon={app}\{#MyAppExeName} 34 | Compression=lzma 35 | SolidCompression=yes 36 | WizardStyle=modern 37 | 38 | [Languages] 39 | Name: "english"; MessagesFile: "compiler:Default.isl" 40 | Name: "french"; MessagesFile: "compiler:Languages\French.isl" 41 | Name: "german"; MessagesFile: "compiler:Languages\German.isl" 42 | Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" 43 | Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" 44 | Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl" 45 | Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" 46 | 47 | [Tasks] 48 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce 49 | 50 | [Files] 51 | Source: "{#AppRepPath}\dist\main\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 52 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 53 | 54 | [Icons] 55 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 56 | Name: "{group}\EarQuiz"; Filename: "{app}\{#MyAppExeName}" 57 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 58 | 59 | [Run] 60 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 61 | 62 | [UninstallDelete] 63 | Type: files; Name: "{userappdata}\EarQuiz\Frequencies\config.ini" 64 | -------------------------------------------------------------------------------- /GUI/MainWindow/View/audiodevices_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import QObject 18 | from PyQt6.QtGui import QActionGroup 19 | from PyQt6.QtMultimedia import QMediaDevices 20 | from application import MediaDevices, Settings 21 | 22 | 23 | class AudioDevicesView(QObject): 24 | def __init__(self, parent): 25 | super().__init__() 26 | self.mw = parent 27 | self.audio_devices = MediaDevices 28 | self.default_name = 'System Sound Output Device' 29 | self.mw.AudioDevicesGroup = QActionGroup(self) 30 | self.setAudioDeviceActions() 31 | self.audio_devices.audioOutputsChanged.connect(self.updateAudioDeviceActions) 32 | self.selectOutput(Settings.value('Actions/SelectedAudioOut', self.default_name)) 33 | 34 | def setAudioDeviceActions(self): 35 | audio_outs = [out.description() for out in QMediaDevices.audioOutputs()] 36 | audio_outs.insert(0, self.default_name) 37 | for AO in audio_outs: 38 | action = self.mw.menuAudio_Device.addAction(AO) 39 | self.mw.AudioDevicesGroup.addAction(action) 40 | action.setCheckable(True) 41 | 42 | def updateAudioDeviceActions(self): 43 | items = self.mw.menuAudio_Device.actions() 44 | checked_act = self.mw.AudioDevicesGroup.checkedAction().text() if self.mw.AudioDevicesGroup.checkedAction() else None 45 | for item in items: 46 | self.mw.AudioDevicesGroup.removeAction(item) 47 | self.mw.menuAudio_Device.removeAction(item) 48 | self.setAudioDeviceActions() 49 | if checked_act: 50 | self.selectOutput(checked_act) 51 | 52 | def selectOutput(self, name: str): 53 | items = self.mw.menuAudio_Device.actions() 54 | for act in items: 55 | if act.text() == name and not act.isChecked(): 56 | act.toggle() 57 | return 58 | self.selectOutput(self.default_name) 59 | 60 | def selectedOutput(self): 61 | outputs = self.audio_devices.audioOutputs() 62 | current = self.mw.AudioDevicesGroup.checkedAction() 63 | if current.text() == self.default_name: 64 | return self.audio_devices.defaultAudioOutput() 65 | for device in outputs: 66 | if device.description() == current.text(): 67 | return device 68 | -------------------------------------------------------------------------------- /GUI/ExScoreInfo/exscoreinfo_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from Utilities.freq2str import freqString 18 | from GUI.MainWindow.View.dark_theme import green_color 19 | from GUI.Misc.colorStr import colorStr 20 | 21 | 22 | class ExScoreInfoView: 23 | ExNum_t = 'Example:' 24 | UserAnsw_t = 'Your answer:' 25 | CorAnsw_t = 'Right answer:' 26 | AnswScore_t = 'Answer score:' 27 | TotalScore_t = 'Total score:' 28 | TestStatus_t = 'Test status:' 29 | 30 | def __init__(self, mw_view): 31 | self.mw_view = mw_view 32 | self.ExNum = mw_view.ExampleNLab 33 | self.UserAnsw = mw_view.UserAnswerLab 34 | self.CorAnsw = mw_view.CorAnswerLab 35 | self.AnswScore = mw_view.AnswerScoreLab 36 | self.TotalScore = mw_view.TotalScoreLab 37 | self.TestStatus = mw_view.TestStatusLab 38 | self.init_texts() 39 | 40 | def init_texts(self, onlyLastExcInfo=False): 41 | self.ExNum.setText(self.ExNum_t) 42 | self.UserAnsw.setText(self.UserAnsw_t) 43 | self.CorAnsw.setText(self.CorAnsw_t) 44 | self.AnswScore.setText(self.AnswScore_t) 45 | if onlyLastExcInfo: 46 | return 47 | self.TotalScore.setText(self.TotalScore_t) 48 | self.TestStatus.setText(self.TestStatus_t) 49 | 50 | def showExNum(self, value): 51 | self.ExNum.setText(f'{self.ExNum_t} {value}/10') if value else '' 52 | 53 | def showUserAnsw(self, value): 54 | self.UserAnsw.setText(f'{self.UserAnsw_t} {freqString(value)}') 55 | 56 | def showCorAnsw(self, value): 57 | self.CorAnsw.setText(f'{self.CorAnsw_t} {freqString(value)}') 58 | 59 | def showAnswScore(self, value: int or float or None): 60 | shown_value = f'{value}/10' if value is not None else '' 61 | self.AnswScore.setText(f'{self.AnswScore_t} {shown_value}') 62 | 63 | def showTotalScore(self, value: int or float, underlined=False): 64 | text = f'{self.TotalScore_t} {value}/100' 65 | text = f'{text}' if underlined else text 66 | self.TotalScore.setText(text) 67 | 68 | def showStatus(self, status: str): 69 | if 'passed' in status or 'progress' in status: 70 | status = colorStr(status, green_color()) 71 | elif 'failed' in status or 'canceled' in status: 72 | status = colorStr(status, 'red') 73 | self.TestStatus.setText(f'{self.TestStatus_t} {status}') 74 | -------------------------------------------------------------------------------- /GUI/ExScoreInfo/exscoreinfo_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from GUI.ExScoreInfo.exscoreinfo_view import ExScoreInfoView 18 | from Model.scorecalc import ScoreCalculator as SC 19 | from Model.scorecalc import expected_results as ER 20 | from Model.scorecalc import passed_failed as PF 21 | 22 | 23 | class ExScoreInfoContr: 24 | CurTest: SC 25 | 26 | def __init__(self, mw_contr): 27 | self.mw_contr = mw_contr 28 | self.view = ExScoreInfoView(mw_contr.mw_view) 29 | self.refresh() 30 | 31 | @property 32 | def test_status(self): 33 | if 0 <= len(self.CurTest.ScoreList) < 10: 34 | if self.mw_contr.CurrentMode.name == 'Test': 35 | return 'in progress' 36 | elif len(self.CurTest.ScoreList) > 0: 37 | return 'canceled' 38 | return self._detectPassedFailed() if len(self.CurTest.ScoreList) == 10 else '' 39 | 40 | def _detectPassedFailed(self): 41 | if len(self.CurTest.ScoreList) < 10: 42 | return '' 43 | eq_pattern = self.mw_contr.EQContr.EQpattern 44 | eq_type = 1 if eq_pattern['EQtype'] == 'EQ1' else 2 45 | er = ER(eq_type, eq_pattern['DualBandMode']) 46 | return PF(self.CurTest.totalScore, er) 47 | 48 | def refresh(self, onlyLastExcInfo=False): 49 | self.CurTest = SC() 50 | self.view.init_texts(onlyLastExcInfo=onlyLastExcInfo) 51 | 52 | def nextEx(self): 53 | self.view.showExNum(self.CurTest.next_ex_num) 54 | self.view.showUserAnsw('') 55 | self.view.showCorAnsw('') 56 | self.view.showAnswScore(None) 57 | 58 | def onAnswerAccepted(self, RightAnswer: int or tuple, UserAnswer: int or tuple): 59 | self.CurTest.input(RightAnswer, UserAnswer) 60 | self.showScore() 61 | self.view.showCorAnsw(RightAnswer) 62 | self.view.showUserAnsw(UserAnswer) 63 | 64 | def showScore(self): 65 | if self.CurTest.ScoreList: 66 | self.view.showAnswScore(self.CurTest.ScoreList[-1][2]) 67 | self.view.showTotalScore(self.CurTest.totalScore, underlined=len(self.CurTest.ScoreList) == 10) 68 | 69 | def showTestStatus(self, reset_mark=True): 70 | if not reset_mark and self.markInStr(self.view.TestStatus.text()): 71 | return 72 | self.view.showStatus(self.test_status) 73 | 74 | @staticmethod 75 | def markInStr(value: str): 76 | return 'passed' in value or 'failed' in value 77 | -------------------------------------------------------------------------------- /GUI/TransportPanel/volumeslider_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtWidgets import QMenu 18 | from PyQt6.QtMultimedia import QAudio 19 | 20 | 21 | class VolumeSliderContr(): 22 | VolumeSliderContextMenu: QMenu 23 | 24 | def __init__(self, parent): # parent: PlayerContr 25 | self.parent = parent 26 | self.mw_view = parent.mw_view 27 | self.VolumeSlider = parent.VolumeSlider 28 | self.savedVolume = None 29 | self.mw_view.actionIncrease_Volume.triggered.connect(self.increaseVolume) 30 | self.mw_view.actionDecrease_Volume.triggered.connect(self.decreaseVolume) 31 | self.mw_view.actionSave_Volume_Level.triggered.connect(self.onSaveVolumeTriggered) 32 | self.mw_view.actionRestore_Volume_Level.triggered.connect(self.onRestoreVolumeTriggered) 33 | self.mw_view.actionRestore_Volume_Level.setEnabled(False) 34 | self._createContextMenu() 35 | self.VolumeSlider.valueChanged.connect(self.applyVolume) 36 | self.VolumeSlider.customContextMenuRequested.connect(self.onVolumeCustomContextMenuRequested) 37 | 38 | def increaseVolume(self): 39 | self.VolumeSlider.setValue(self.mw_view.VolumeSlider.value() + 5) 40 | 41 | def decreaseVolume(self): 42 | self.VolumeSlider.setValue(self.mw_view.VolumeSlider.value() - 5) 43 | 44 | def applyVolume(self, volumeSliderValue): 45 | linearVolume = QAudio.convertVolume(volumeSliderValue / 100, 46 | QAudio.VolumeScale.LogarithmicVolumeScale, 47 | QAudio.VolumeScale.LinearVolumeScale) 48 | self.parent.audioOutput.setVolume(linearVolume) 49 | 50 | def _createContextMenu(self): 51 | self.VolumeSliderContextMenu = QMenu() 52 | self.VolumeSliderContextMenu.addActions((self.mw_view.actionSave_Volume_Level, 53 | self.mw_view.actionRestore_Volume_Level)) 54 | 55 | def onVolumeCustomContextMenuRequested(self, pos): 56 | self.VolumeSliderContextMenu.exec(self.VolumeSlider.mapToGlobal(pos)) 57 | 58 | def onSaveVolumeTriggered(self): 59 | self.savedVolume = self.parent.VolumeSlider.value() 60 | self.mw_view.actionRestore_Volume_Level.setEnabled(True) 61 | 62 | def onRestoreVolumeTriggered(self): 63 | if self.savedVolume is None: 64 | return 65 | self.parent.VolumeSlider.setValue(self.savedVolume) 66 | -------------------------------------------------------------------------------- /Model/AudioEngine/pinknoise_gen.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # The 'spectrum_noise' and 'pink_spectrum' functions were taken from: 18 | # https://www.socsci.ru.nl/wilberth/python/noise.html 19 | # Copyright (C) 2012-2023 Wilbert van Ham, Stichting Katholieke Universiteit, KVK 41055629, Nijmegen 20 | # License: General Public License version 3 or later. 21 | 22 | 23 | import numpy as np 24 | 25 | 26 | def spectrum_noise(spectrum_func, samples=1024, rate=44100): 27 | """ 28 | make noise with a certain spectral density 29 | """ 30 | freqs = np.fft.rfftfreq(samples, 1.0 / rate) # real-fft frequencies (not the negative ones) 31 | spectrum = np.zeros_like(freqs, dtype='complex') # make complex numbers for spectrum 32 | spectrum[1:] = spectrum_func(freqs[1:]) # get spectrum amplitude for all frequencies except f=0 33 | phases = np.random.uniform(0, 2 * np.pi, len(freqs) - 1) # random phases for all frequencies except f=0 34 | spectrum[1:] *= np.exp(1j * phases) # apply random phases 35 | noise = np.fft.irfft(spectrum) # return the reverse fourier transform 36 | noise = np.pad(noise, (0, samples - len(noise)), 'constant') # add zero for odd number of input samples 37 | 38 | return noise 39 | 40 | 41 | def pink_spectrum(f, f_min=0, f_max=np.inf, att=np.log10(2.0) * 10): 42 | """ 43 | Define a pink (1/f) spectrum 44 | f = array of frequencies 45 | f_min = minimum frequency for band pass 46 | f_max = maximum frequency for band pass 47 | att = attenuation per factor two in frequency in decibel. 48 | Default is such that a factor two in frequency increase gives a factor two in power attenuation. 49 | """ 50 | # numbers in the equation below explained: 51 | # 0.5: take the square root of the power spectrum so that we get an amplitude (field) spectrum 52 | # 10.0: convert attenuation from decibel to bel 53 | # 2.0: frequency factor for which the attenuation is given (octave) 54 | s = f ** -(0.5 * (att / 10.0) / np.log10(2.0)) # apply attenuation 55 | s[np.logical_or(f < f_min, f > f_max)] = 0 # apply band pass 56 | return s 57 | 58 | 59 | def generate_pinknoise(length_s=30): 60 | pn = spectrum_noise(lambda x: pink_spectrum(x, 20, 20000), samples=44100 * length_s) 61 | pn = pn / max(abs(pn)) * 0.8 # adjusting gain level 62 | pn.resize((1, 44100 * length_s)) # resizing/reshaping array to fit pedalboard requirements 63 | return pn 64 | -------------------------------------------------------------------------------- /GUI/TransportPanel/transport_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from GUI.TransportPanel.audioslider_view import AudioSliderView 18 | from GUI.TransportPanel.cropregiontimestr import CropRegionTimestr 19 | from GUI.TransportPanel.player_view import PlayerView 20 | from Utilities.common_calcs import hhmmss 21 | from PyQt6 import QtCore 22 | 23 | 24 | class TransportPanelView: 25 | def __init__(self, mw_view): 26 | self.mw_view = mw_view 27 | self.mw_view.TransportPanel.setMinimumSize(QtCore.QSize(0, 115)) 28 | self.PlayerView = PlayerView(self.mw_view) 29 | self.AudioSliderView = AudioSliderView(self.mw_view.AudioSlider) 30 | self.Duration_Lab = mw_view.Duration_Lab 31 | self.Position_Lab = mw_view.Position_Lab 32 | self.SliceLenSpin = mw_view.SliceLenSpin 33 | self.SlicesNum_Lab = mw_view.SlicesNum_Lab 34 | self.StartTimeEdit = mw_view.StartTimeEdit 35 | self.EndTimeEdit = mw_view.EndTimeEdit 36 | self.StartPointBut = mw_view.StartPointBut 37 | self.EndPointBut = mw_view.EndPointBut 38 | self.RangeToStart = mw_view.RangeToStart 39 | self.RangeToEnd = mw_view.RangeToEnd 40 | self.ClearRangeBut = mw_view.ClearRangeBut 41 | self.CropRegionTstr = CropRegionTimestr(self) 42 | self.SaveSliceLengthAsDefault = mw_view.SaveSliceLengthAsDefault 43 | self.setSpinStyles() 44 | self.setHeader() 45 | 46 | def setSpinStyles(self): 47 | self.StartTimeEdit.setStyleSheet(self.mw_view.TimeSpinStyle) 48 | self.EndTimeEdit.setStyleSheet(self.mw_view.TimeSpinStyle) 49 | 50 | def setHeader(self, audio_name='No audio'): 51 | self.mw_view.TransportPanel.setWindowTitle(f'Transport Panel: {audio_name}') 52 | 53 | def setSlicesNum(self, value: int): 54 | self.SlicesNum_Lab.setText(f'Number of Slices: {value}') 55 | 56 | def noSongState(self): 57 | self.setHeader() 58 | self.AudioSliderView.SliceRegion.hide() 59 | self.AudioSliderView.CropRegion.hide() 60 | self.AudioSliderView.Cursor.hide() 61 | self.CropRegionTstr.noAudioState(True) 62 | zero_time_str = hhmmss(0) 63 | self.Position_Lab.setText(zero_time_str) 64 | self.Duration_Lab.setText(zero_time_str) 65 | self.setSlicesNum(0) 66 | 67 | def setDurationLabValue(self, value: int or float): # value in sec 68 | self.Duration_Lab.setText(hhmmss(value)) 69 | 70 | def setPositionLabValue(self, value: int or float): # value in sec 71 | self.Position_Lab.setText(hhmmss(value)) 72 | -------------------------------------------------------------------------------- /GUI/Playlist/PLLoadDialog.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from multiprocessing import Process, Manager 18 | from PyQt6.QtCore import QObject, Qt, QRunnable, pyqtSignal, pyqtSlot, QThreadPool 19 | from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QVBoxLayout 20 | from Model.FileLinksParser import pathsResolve 21 | 22 | 23 | class PLLoadSignals(QObject): 24 | finished = pyqtSignal() 25 | error = pyqtSignal(str) 26 | 27 | 28 | class PLLoadChecker(QRunnable): 29 | signals = PLLoadSignals() 30 | 31 | def __init__(self, process): 32 | super().__init__() 33 | self.process = process 34 | self._killed = False 35 | 36 | @pyqtSlot() 37 | def run(self): 38 | while self.process.is_alive() and not self._killed: 39 | pass 40 | if self._killed: 41 | return 42 | self.signals.finished.emit() 43 | 44 | def kill(self): 45 | self._killed = True 46 | 47 | 48 | class PLProcDialog(QDialog): 49 | threadpool: QThreadPool 50 | process_check_run: PLLoadChecker 51 | 52 | def __init__(self, paths: list[str]): 53 | super().__init__() 54 | self.setWindowFlags(Qt.WindowType.SplashScreen) 55 | self.paths = paths 56 | self.setWindowTitle("Please, wait...") 57 | self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel) 58 | self.buttonBox.rejected.connect(self.reject) 59 | self.layout = QVBoxLayout() 60 | self.label = QLabel("Adding audiofile(s) to playlist...") 61 | self.label.setAlignment(Qt.AlignmentFlag.AlignHCenter) 62 | self.layout.addWidget(self.label) 63 | self.layout.addWidget(self.buttonBox) 64 | self.setLayout(self.layout) 65 | self.return_dict = Manager().dict() 66 | self.process = Process(target=pathsResolve, args=(self.paths, self.return_dict), daemon=True) 67 | self.start_process() 68 | 69 | def start_process(self): 70 | self.process.start() 71 | self.process_check_run = PLLoadChecker(self.process) 72 | self.process_check_run.signals.finished.connect(self.on_finished, type=Qt.ConnectionType.SingleShotConnection) 73 | self.threadpool = QThreadPool() 74 | self.threadpool.setMaxThreadCount(1) 75 | self.threadpool.start(self.process_check_run) 76 | 77 | def on_finished(self): 78 | self.accept() 79 | 80 | def reject(self): 81 | self.process_check_run.kill() 82 | if self.threadpool.activeThreadCount() > 0: 83 | self.threadpool.waitForDone() 84 | self.process.terminate() 85 | self.process.join() 86 | super(PLProcDialog, self).reject() 87 | -------------------------------------------------------------------------------- /GUI/SupportApp/supportapp_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from urllib import parse 18 | from application import app 19 | import webbrowser 20 | 21 | 22 | class SupportAppContr: 23 | def __init__(self, mw_contr): 24 | self.sharedURL = 'https://earquiz.org/EQ_Frequencies/' 25 | self.sharedURL_q = parse.quote(self.sharedURL, encoding='utf-8') 26 | self.sharedText = parse.quote("Great free software for technical ear " 27 | "training on equalization under Windows, macOS, and Linux: ") 28 | self.mw_contr = mw_contr 29 | self.mw_view = mw_contr.mw_view 30 | self.mw_view.Facebook.clicked.connect(self.onFacebook_clicked) 31 | self.mw_view.VK.clicked.connect(self.onVK_clicked) 32 | self.mw_view.Twitter.clicked.connect(self.onTwitter_clicked) 33 | self.mw_view.BMC.clicked.connect(self.onBMC_clicked) 34 | self.mw_view.Patreon.clicked.connect(self.onPatreon_clicked) 35 | self.mw_view.Boosty.clicked.connect(self.onBoosty_clicked) 36 | self.mw_view.CopyLink.clicked.connect(self.onCopyLink_clicked) 37 | self.mw_view.WhatsApp.clicked.connect(self.onWhatsApp_clicked) 38 | self.mw_view.Telegram.clicked.connect(self.onTelegram_clicked) 39 | self.mw_view.Reddit.clicked.connect(self.onReddit_clicked) 40 | self.mw_view.BMC.setVisible(False) 41 | 42 | def onFacebook_clicked(self): 43 | webbrowser.open(f'https://www.facebook.com/sharer/sharer.php?u={self.sharedURL_q}') 44 | 45 | def onTwitter_clicked(self): 46 | webbrowser.open(f'https://twitter.com/intent/tweet?text={self.sharedText}{self.sharedURL_q}') 47 | 48 | def onCopyLink_clicked(self): 49 | app.clipboard().setText(self.sharedURL) 50 | self.mw_view.status.TempLabel.update('Link to EarQuiz Frequencies copied to clipboard.') 51 | 52 | def onBMC_clicked(self): 53 | webbrowser.open('https://www.buymeacoffee.com/gdalik') 54 | 55 | def onPatreon_clicked(self): 56 | webbrowser.open('https://www.patreon.com/EarQuiz') 57 | 58 | def onBoosty_clicked(self): 59 | webbrowser.open('https://boosty.to/earquiz') 60 | 61 | def onWhatsApp_clicked(self): 62 | webbrowser.open(f'https://api.whatsapp.com/send/?text={self.sharedText}{self.sharedURL_q}') 63 | 64 | def onTelegram_clicked(self): 65 | webbrowser.open(f'https://t.me/share/url?url={self.sharedURL_q}&text={self.sharedText}') 66 | 67 | def onVK_clicked(self): 68 | webbrowser.open(f'https://vk.com/share.php?url={self.sharedURL}') 69 | 70 | def onReddit_clicked(self): 71 | webbrowser.open(f'https://reddit.com/submit?url={self.sharedURL_q}&title={self.sharedText}') 72 | -------------------------------------------------------------------------------- /GUI/icons.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icons/AddRemove/clear.png 4 | Icons/AddRemove/clear_neg-disabled.png 5 | Icons/AddRemove/clear_neg.png 6 | Icons/Misc/next-pattern.png 7 | Icons/AddRemove/minus.png 8 | Icons/AddRemove/plus.png 9 | 10 | 11 | Icons/Support_Logos/bmc-logo.png 12 | Icons/Support_Logos/facebook.png 13 | Icons/Support_Logos/linked.png 14 | Icons/Support_Logos/linked_neg.png 15 | Icons/Support_Logos/reddit.png 16 | Icons/Support_Logos/telegram.png 17 | Icons/Support_Logos/twitter.png 18 | Icons/Support_Logos/vk.png 19 | Icons/Support_Logos/WhatsApp.png 20 | Icons/Support_Logos/Patreon.png 21 | Icons/Support_Logos/Boosty.png 22 | 23 | 24 | Icons/Misc/Negative/padlock.png 25 | Icons/Misc/Negative/Settings.png 26 | Icons/Misc/Negative/star.png 27 | Icons/Misc/Negative/unlock.png 28 | Icons/Misc/padlock.png 29 | Icons/Misc/Settings.png 30 | Icons/Misc/star.png 31 | Icons/Misc/unlock.png 32 | 33 | 34 | Icons/Player/Negative/arrow-right_gray.png 35 | Icons/Player/shuffle_blue-disabled.png 36 | Icons/Player/Negative/shuffle_black-disabled.png 37 | Icons/Player/Negative/arrow-right_gray-disabled.png 38 | Icons/Player/Negative/left-arrow-playlist-disabled.png 39 | Icons/Player/Negative/right-arrow-playlist-disabled.png 40 | Icons/Player/Negative/left-arrow-playlist.png 41 | Icons/Player/Negative/music-note-with-loop-circular-arrows-around.png 42 | Icons/Player/Negative/right-arrow-playlist.png 43 | Icons/Player/Negative/sequence - black.png 44 | Icons/Player/Negative/shuffle_black.png 45 | Icons/Player/Actions-media-playback-pause-icon.png 46 | Icons/Player/left-arrow-playlist.png 47 | Icons/Player/right-arrow-playlist.png 48 | Icons/Player/Actions-media-playback-start-icon.png 49 | Icons/Player/Actions-media-playback-stop-icon.png 50 | Icons/Player/Actions-media-seek-backward-icon.png 51 | Icons/Player/Actions-media-seek-forward-icon.png 52 | Icons/Player/Actions-media-skip-backward-icon.png 53 | Icons/Player/CurrentSong.png 54 | Icons/Player/Actions-media-skip-forward-icon.png 55 | Icons/Player/arrow-right_gray.png 56 | Icons/Player/music-note-with-loop-circular-arrows-around-blue.png 57 | Icons/Player/music-note-with-loop-circular-arrows-around.png 58 | Icons/Player/sequence - black.png 59 | Icons/Player/sequence - blue.png 60 | Icons/Player/shuffle_black.png 61 | Icons/Player/shuffle_blue.png 62 | 63 | 64 | Icons/Logo/EarQuiz_Icon.png 65 | Icons/Logo/EarQuiz_Header.png 66 | Icons/Logo/EarQuiz_Splash.png 67 | 68 | 69 | -------------------------------------------------------------------------------- /GUI/AudioProcSettings/audio_proc_settings_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | from PyQt6.QtCore import QObject 19 | import Model.AudioEngine.audio_proc_settings as APS 20 | from Model.AudioEngine.audio_proc_settings import default_EQOnTimePerc, default_EQTransitionDur, default_ExFadeInOutDur 21 | 22 | 23 | class AudioProcSettingsContr(QObject): 24 | def __init__(self, mw_contr): 25 | super().__init__() 26 | self.mw_contr = mw_contr 27 | self.mw_view = mw_contr.mw_view 28 | self.APSV = self.mw_view.AudioProcSettingsView 29 | self.mw_view.actionAudio_Processing_Settings.triggered.connect(self.on_actionAudio_Processing_Settings_clicked) 30 | self.APSV.ResetBut.clicked.connect(self.on_ResetBut_clicked) 31 | self.APSV.ApplyBut.clicked.connect(self.on_ApplyBut_clicked) 32 | self.mw_view.actionEQ_Always_On_In_Test_Mode.toggled.connect(self.on_actionEQ_Always_On_In_Test_Mode_toggled) 33 | 34 | def on_actionAudio_Processing_Settings_clicked(self): 35 | if self.APSV.isVisible(): 36 | self.APSV.setFocus() 37 | self.APSV.activateWindow() 38 | else: 39 | self.APSV.show() 40 | 41 | def on_ResetBut_clicked(self): 42 | self.APSV.EQOnTimeSlider.setValue(default_EQOnTimePerc) 43 | self.APSV.EQOnOffTransSpin.setValue(int(default_EQTransitionDur * 1000)) 44 | self.APSV.FadeInOutDurSpin.setValue(int(default_ExFadeInOutDur * 1000)) 45 | 46 | def on_ApplyBut_clicked(self): 47 | APS.setEQOnTimePerc(self.APSV.EQOnTimeSlider.value()) 48 | APS.setEQTransitionDur(self.APSV.EQOnOffTransSpin.value()) 49 | APS.setExFadeInOutDur(self.APSV.FadeInOutDurSpin.value()) 50 | self.APSV.setApplyButState() 51 | if self.mw_contr.ADGen is not None: 52 | self.mw_contr.ADGen.proc_t_perc = APS.getEQOnTimePerc() 53 | if self.mw_contr.CurrentMode.name != 'Preview': 54 | self.refreshAudio() 55 | 56 | def on_actionEQ_Always_On_In_Test_Mode_toggled(self, v): 57 | APS.setEQAlwaysOnInTest(v) 58 | if self.mw_contr.CurrentMode.name == 'Test': 59 | self.mw_contr.TransportContr.PlayerContr.onStopTriggered() 60 | 61 | def refreshAudio(self): 62 | self.mw_contr.TransportContr.PlayerContr.onStopTriggered() 63 | self.mw_contr.TransportContr.refreshAudio() 64 | 65 | def updADGenEQOnTimeToSit(self): 66 | if self.mw_contr.ADGen is None: 67 | return 68 | self.mw_contr.ADGen.proc_t_perc = self.sit_proc_t_perc 69 | 70 | @property 71 | def sit_proc_t_perc(self): # Situational proc_t_perc (EQ On Time) value 72 | return 100 if self.mw_contr.CurrentMode.name == 'Test' and \ 73 | self.mw_view.actionEQ_Always_On_In_Test_Mode.isChecked() else APS.getEQOnTimePerc() 74 | -------------------------------------------------------------------------------- /GUI/Playlist/ContextMenu.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtGui import QAction 18 | from PyQt6.QtWidgets import QMenu 19 | from Utilities.selectFileInSysExplorer import selectFile 20 | from GUI.Misc.filemanager_name import fn 21 | 22 | 23 | class PLContextMenu(QMenu): 24 | actionLoad: QAction 25 | actionShowFile: QAction 26 | actionConvertAudio: QAction 27 | actionRemove: QAction 28 | actionRemoveUnavailable: QAction 29 | 30 | def __init__(self, parent): # parent: PlaylistContr 31 | super().__init__() 32 | self.parent = parent 33 | self.playlistdata = self.parent.playlistModel.playlistdata 34 | self.selected_tracks = self.parent.PlaylistView.selectedItems 35 | self.createActions() 36 | self.menuCreated = False 37 | self.createMenu() 38 | 39 | def createActions(self): 40 | self.actionLoad = QAction('Load in Preview Mode', self) 41 | self.actionShowFile = QAction(f'Show in {fn()}') 42 | self.actionShowFile.triggered.connect(self.showSelectedFile) 43 | file_word = 'File' if len(self.selected_tracks) == 1 else 'Files' 44 | self.actionConvertAudio = QAction(f'Convert Selected {file_word}...') 45 | self.actionRemove = QAction('Remove Selected') 46 | self.actionRemoveUnavailable = QAction('Remove Unavailable') 47 | 48 | def createMenu(self): 49 | if len(self.playlistdata) == 0: 50 | return 51 | if len(self.selected_tracks) == 1: 52 | self._createSingleSelectedTrackMenu() 53 | remove_sep = False 54 | if len(self.selected_tracks) >= 1: 55 | remove_sep = True 56 | self._createMultipleSelectedTrackMenu() 57 | if not remove_sep: 58 | self.addSeparator() 59 | unavailable_present = any((not S.available for S in self.playlistdata)) 60 | self.actionRemoveUnavailable.setEnabled(unavailable_present) 61 | if unavailable_present or len(self.selected_tracks) > 0: 62 | self.addAction(self.actionRemoveUnavailable) 63 | self.menuCreated = True 64 | 65 | def _createSingleSelectedTrackMenu(self): 66 | self.addAction(self.actionLoad) 67 | self.addSeparator() 68 | self.addAction(self.actionShowFile) 69 | exists = self.selected_tracks[0].exists 70 | self.actionLoad.setEnabled(exists) 71 | self.actionShowFile.setEnabled(exists) 72 | self.menuCreated = True 73 | 74 | def _createMultipleSelectedTrackMenu(self): 75 | self.actionConvertAudio.setEnabled(any((P.exists for P in self.selected_tracks))) 76 | self.addAction(self.actionConvertAudio) 77 | self.addSeparator() 78 | self.addAction(self.actionRemove) 79 | self.menuCreated = True 80 | 81 | def showSelectedFile(self): 82 | filepath = self.selected_tracks[0].path 83 | selectFile(filepath) 84 | -------------------------------------------------------------------------------- /Model/Data/eq_patterns.json: -------------------------------------------------------------------------------- 1 | { 2 | "Patterns": [ 3 | { 4 | "Name": "Lowest five (31-500 Hz) 1-octave bands boosted (+)", 5 | "EQtype": "EQ1", 6 | "ActiveFreqRange": [ 7 | 31, 8 | 500 9 | ], 10 | "EQ_boost_cut": "+" 11 | }, 12 | { 13 | "Name": "Middle five (250-4000 Hz) 1-octave bands boosted (+)", 14 | "EQtype": "EQ1", 15 | "ActiveFreqRange": [ 16 | 250, 17 | 4000 18 | ], 19 | "EQ_boost_cut": "+" 20 | }, 21 | { 22 | "Name": "Highest five (1-16 kHz) 1-octave bands boosted (+)", 23 | "EQtype": "EQ1", 24 | "ActiveFreqRange": [ 25 | 1000, 26 | 16000 27 | ], 28 | "EQ_boost_cut": "+" 29 | }, 30 | { 31 | "Name": "Lowest five (31-500 Hz) 1-octave bands cut (-)", 32 | "EQtype": "EQ1", 33 | "ActiveFreqRange": [ 34 | 31, 35 | 500 36 | ], 37 | "EQ_boost_cut": "-" 38 | }, 39 | { 40 | "Name": "Middle five (250-4000 Hz) 1-octave bands cut (-)", 41 | "EQtype": "EQ1", 42 | "ActiveFreqRange": [ 43 | 250, 44 | 4000 45 | ], 46 | "EQ_boost_cut": "-" 47 | }, 48 | { 49 | "Name": "Highest five (1-16 kHz) 1-octave bands cut (-)", 50 | "EQtype": "EQ1", 51 | "ActiveFreqRange": [ 52 | 1000, 53 | 16000 54 | ], 55 | "EQ_boost_cut": "-" 56 | }, 57 | { 58 | "Name": "All ten 1-octave bands boosted (+)", 59 | "EQtype": "EQ1", 60 | "ActiveFreqRange": [ 61 | 31, 62 | 16000 63 | ], 64 | "EQ_boost_cut": "+" 65 | }, 66 | { 67 | "Name": "All ten 1-octave bands cut (-)", 68 | "EQtype": "EQ1", 69 | "ActiveFreqRange": [ 70 | 31, 71 | 16000 72 | ], 73 | "EQ_boost_cut": "-" 74 | }, 75 | { 76 | "Name": "All ten 1-octave bands boosted (+) or cut (-)", 77 | "EQtype": "EQ1", 78 | "ActiveFreqRange": [ 79 | 31, 80 | 16000 81 | ], 82 | "EQ_boost_cut": "+-" 83 | }, 84 | { 85 | "Name": "Low (31-315 Hz) 1/3-octave bands boosted (+) or cut (-)", 86 | "EQtype": "EQ2", 87 | "ActiveFreqRange": [ 88 | 31, 89 | 315 90 | ], 91 | "EQ_boost_cut": "+-" 92 | }, 93 | { 94 | "Name": "Mid (250-2500 Hz) 1/3-octave bands boosted (+) or cut (-)", 95 | "EQtype": "EQ2", 96 | "ActiveFreqRange": [ 97 | 250, 98 | 2500 99 | ], 100 | "EQ_boost_cut": "+-" 101 | }, 102 | { 103 | "Name": "High (1.6-16 kHz) 1/3-octave bands boosted (+) or cut (-)", 104 | "EQtype": "EQ2", 105 | "ActiveFreqRange": [ 106 | 1600, 107 | 16000 108 | ], 109 | "EQ_boost_cut": "+-" 110 | }, 111 | { 112 | "Name": "All 1/3-octave bands boosted (+) or cut (-)", 113 | "EQtype": "EQ2", 114 | "ActiveFreqRange": [ 115 | 31, 116 | 16000 117 | ], 118 | "EQ_boost_cut": "+-" 119 | }, 120 | { 121 | "Name": "Two 1-octave bands treated, each one boosted (+) or cut (-)", 122 | "EQtype": "EQ1", 123 | "ActiveFreqRange": [ 124 | 31, 125 | 16000 126 | ], 127 | "EQ_boost_cut": "+-", 128 | "DualBandMode": true, 129 | "DisableAdjacentFiltersMode": 1 130 | }, 131 | { 132 | "Name": "Two 1/3-octave bands treated, each one boosted (+) or cut (-)", 133 | "EQtype": "EQ2", 134 | "ActiveFreqRange": [ 135 | 31, 136 | 16000 137 | ], 138 | "EQ_boost_cut": "+-", 139 | "DualBandMode": true, 140 | "DisableAdjacentFiltersMode": 4 141 | } 142 | ] 143 | } 144 | -------------------------------------------------------------------------------- /GUI/UpdateChecker/update_checker_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import QObject, QThreadPool, Qt 18 | from GUI.UpdateChecker.update_checker_runner import UpdCheckRun 19 | from application import Settings 20 | import datetime 21 | 22 | 23 | class UpdCheckContr(QObject): 24 | threadpool: QThreadPool 25 | UpdCheckRun: UpdCheckRun or None 26 | minAutoUpdInterval_days = 7 27 | 28 | def __init__(self, mw_contr): 29 | super().__init__() 30 | self.mw_contr = mw_contr 31 | self.mw_view = mw_contr.mw_view 32 | self.mw_view.actionCheck_for_Updates.triggered.connect(self.checkUpdates_manual) 33 | self.UpdCheckRun = None 34 | self.manual_call = False 35 | self.checkUpdates_auto() 36 | 37 | def checkUpdates_auto(self): 38 | days_passed = self.daysSinceLastUpdCheck() 39 | if days_passed is None or days_passed >= self.minAutoUpdInterval_days: 40 | self.checkUpdates() 41 | 42 | def checkUpdates_manual(self): 43 | self.manual_call = True 44 | self.checkUpdates() 45 | 46 | def checkUpdates(self): 47 | if self.UpdCheckRun is not None and self.UpdCheckRun.in_process: 48 | return 49 | self.threadpool = QThreadPool() 50 | self.UpdCheckRun = UpdCheckRun() 51 | self.UpdCheckRun.signals.finished.connect(self.on_finished, type=Qt.ConnectionType.SingleShotConnection) 52 | self.UpdCheckRun.signals.error.connect(self.on_error, type=Qt.ConnectionType.UniqueConnection) 53 | self.threadpool.start(self.UpdCheckRun) 54 | self.mw_view.status.TempLabel.update(shown_text='Checking for updates...', time=-1) 55 | 56 | def on_error(self, msg: str): 57 | self.updCheckStoppedEnded() 58 | print(f'ERROR: {msg}') 59 | 60 | def on_finished(self): 61 | manual_call = self.manual_call 62 | self.updCheckStoppedEnded() 63 | if self.UpdCheckRun.upd_data is None: 64 | return 65 | if self.UpdCheckRun.upd_data == 'no_upd': 66 | self.saveLastSuccessfulCheck() 67 | if manual_call: 68 | self.mw_view.UpdCheckView.noUpdMsg() 69 | return 70 | if not isinstance(self.UpdCheckRun.upd_data, dict): 71 | return 72 | self.mw_view.UpdCheckView.showUpdWindow(self.UpdCheckRun.upd_data) 73 | self.saveLastSuccessfulCheck() 74 | 75 | def updCheckStoppedEnded(self): 76 | self.UpdCheckRun.signals.disconnect() 77 | self.UpdCheckRun.in_process = False 78 | self.manual_call = False 79 | self.mw_view.status.TempLabel.clear() 80 | 81 | @staticmethod 82 | def saveLastSuccessfulCheck(): 83 | Settings.setValue('MainWindow/LastUpdCheck', datetime.datetime.now()) 84 | 85 | @staticmethod 86 | def daysSinceLastUpdCheck(): 87 | last_checked = Settings.value('MainWindow/LastUpdCheck', None) 88 | if last_checked is None: 89 | return None 90 | timedelta = datetime.datetime.now() - last_checked 91 | return timedelta.days 92 | -------------------------------------------------------------------------------- /Model/scorecalc.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import math 18 | 19 | 20 | def expected_results(eq_mode: int, DualBand: bool): # eq_mode 1 -- 1-octave; eq_mode 2 -- 1/3-octave EQ 21 | exp_res = (40, 40) 22 | if eq_mode == 1 and not DualBand: 23 | exp_res = (85, 95) 24 | elif eq_mode == 2 and not DualBand: 25 | exp_res = (75, 90) 26 | elif eq_mode == 1 and DualBand: 27 | exp_res = (70, 85) 28 | elif eq_mode == 2 and DualBand: 29 | exp_res = (65, 80) 30 | return exp_res 31 | 32 | 33 | def passed_failed(user_score: int or float, exp_res: tuple, fail_line=5): 34 | if user_score < exp_res[0] - fail_line: 35 | return 'failed' 36 | elif exp_res[0] - fail_line <= user_score <= exp_res[1]: 37 | return 'passed' 38 | else: 39 | return 'passed+' 40 | 41 | 42 | class ScoreCalculator: 43 | def __init__(self): 44 | self.CurrentRightAnswer = None 45 | self.CurrentUserAnswer = None 46 | self.ScoreList = [] 47 | 48 | def input(self, RightAnswer: int or tuple, UserAnswer: int or tuple): 49 | if len(self.ScoreList) == 10: 50 | raise ValueError('The number of drills in a test cannot exceed 10.') 51 | self.CurrentRightAnswer = RightAnswer 52 | self.CurrentUserAnswer = UserAnswer 53 | self.ScoreList.append((self.CurrentRightAnswer, self.CurrentUserAnswer, self.count())) 54 | 55 | def count(self): 56 | if isinstance(self.CurrentRightAnswer, int): 57 | return self._singleBand_count() 58 | if isinstance(self.CurrentRightAnswer, tuple): 59 | return self._dualBand_count() 60 | 61 | def _singleBand_count(self, max_score=10): 62 | A = self.CurrentRightAnswer 63 | U = self.CurrentUserAnswer 64 | error = math.log(max(abs(A), abs(U)) / min(abs(A), abs(U))) / math.log(2) 65 | rem_error = error - int(error) 66 | rem_error_w = min([0, 0.33, 0.66, 1], key=lambda x: abs(x - rem_error)) 67 | error = int(error) + rem_error_w 68 | boost_cut_error = 2 if abs(A) / A != abs(U) / U else 0 69 | return max(round(max_score - error - boost_cut_error, 2), 0) 70 | 71 | def _dualBand_count(self): 72 | score_list = [] 73 | A = self.CurrentRightAnswer 74 | U = self.CurrentUserAnswer 75 | combinations = (((A[0], U[0]), (A[1], U[1])), ((A[0], U[1]), (A[1], U[0]))) 76 | for comb in combinations: 77 | sc1 = ScoreCalculator() 78 | sc1.input(*comb[0]) 79 | sc1_count = sc1._singleBand_count(max_score=5) 80 | sc2 = ScoreCalculator() 81 | sc2.input(*comb[1]) 82 | sc2_count = sc2._singleBand_count(max_score=5) 83 | score_list.append(max(0, sc1_count) + max(0, sc2_count)) 84 | return round(max(score_list), 2) 85 | 86 | @property 87 | def next_ex_num(self): 88 | count = len(self.ScoreList) 89 | return count + 1 if count < 10 else 0 90 | 91 | @property 92 | def totalScore(self): 93 | return round(sum(x[2] for x in self.ScoreList), 2) if len(self.ScoreList) > 0 else 0 94 | -------------------------------------------------------------------------------- /GUI/About/credits.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | *EarQuiz Frequencies* is written in [Python](https://www.python.org/). The executables for Windows, macOS and Linux are 4 | built with [PyInstaller](https://pyinstaller.org/). 5 | 6 | This application uses the following **Open Source libraries**: 7 | - [PyQt6](https://www.riverbankcomputing.com/software/pyqt/)
8 | Copyright © 2021-2024 Riverbank Computing Limited
9 | License: [GPL v3](https://www.gnu.org/licenses/gpl-3.0.html) 10 | - [Pedalboard](https://spotify.github.io/pedalboard/index.html#)
11 | Copyright © 2021-2024 Spotify AB
12 | License: [GPL v3](https://www.gnu.org/licenses/gpl-3.0.html) 13 | - [NumPy](https://numpy.org/)
14 | Copyright © 2005-2024 NumPy Developers
15 | License: [BSD 3-Clause](https://opensource.org/license/bsd-3-clause/) 16 | - [PyQtGraph](https://www.pyqtgraph.org/)
17 | Copyright © 2012-2024 University of North Carolina at Chapel Hill
18 | License: [MIT](https://opensource.org/license/mit/) 19 | - [tendo](https://pypi.org/project/tendo/)
20 | Copyright © 2010-2022 Sorin Sbarnea
21 | License: [Python Software Foundation License](https://docs.python.org/3/license.html#psf-license) 22 | - [Certifi](https://pypi.org/project/certifi/)
23 | Copyright © 2011-2024 Kenneth Reitz
24 | License: [OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)](https://pypi.org/search/?c=License+%3A%3A+OSI+Approved+%3A%3A+Mozilla+Public+License+2.0+%28MPL+2.0%29) 25 | 26 | ### Icons 27 | 28 | The majority of icons, used in this application, have been taken from [Flaticon](https://www.flaticon.com/): 29 | 30 | Plus icons created by Freepik - Flaticon
31 | Minus icons created by Freepik - Flaticon
32 | Around icons created by Freepik - Flaticon
33 | Lifecycle icons created by Freepik - Flaticon
34 | Settings icons created by Freepik - Flaticon
35 | Failure icons created by Icon mania - Flaticon
36 | Random icons created by mavadee - Flaticon
37 | Star icons created by Taufik - Flaticon
38 | Lock icons created by Maxim Basinski Premium - Flaticon
39 | Left arrow icons created by NajmunNahar - Flaticon
40 | Right arrowhead icons created by NajmunNahar - Flaticon
41 | Arrow icons created by Pixel perfect - Flaticon
42 | Idea icons created by Good Ware - Flaticon
43 | Copy link icons created by feen - Flaticon
44 | Image by rawpixel.com on Freepik
45 | Youtube icons created by Freepik - Flaticon 46 | 47 | Player icons by [The Oxygen Team](https://www.iconarchive.com/icons/oxygen-icons.org/oxygen/authors.txt). -------------------------------------------------------------------------------- /GUI/Playlist/plsong.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from dataclasses import dataclass 18 | from functools import cached_property 19 | from pathlib import PurePath, Path 20 | from pedalboard.io import AudioFile 21 | from Model.globals import MinAudioDuration 22 | from Utilities.common_calcs import mmss 23 | from definitions import PN 24 | 25 | 26 | @dataclass(eq=False) 27 | class PlSong: 28 | inputPath: str 29 | 30 | @cached_property 31 | def isPinkNoise(self): 32 | return self.inputPath == PN 33 | 34 | @cached_property 35 | def path(self): 36 | if self.isPinkNoise: 37 | return self.inputPath 38 | return str(Path(self.inputPath).absolute()) if self.inputPath else '' 39 | 40 | @cached_property 41 | def name(self): 42 | return str(PurePath(self.path).name) 43 | 44 | @cached_property 45 | def dirPath(self): 46 | path = str(PurePath(self.path).parent) 47 | return path if path != '.' else '' 48 | 49 | @property 50 | def exists(self): 51 | return True if self.isPinkNoise else Path(self.path).is_file() 52 | 53 | @cached_property 54 | def file_properties(self): 55 | return_dict = self._default_dict 56 | if not self.exists or self.isPinkNoise: 57 | return return_dict 58 | try: 59 | with AudioFile(self.path) as f: 60 | return_dict['duration'] = f.duration 61 | num_channels = f.num_channels 62 | if num_channels == 1: 63 | num_channels = 'Mono' 64 | elif num_channels == 2: 65 | num_channels = 'Stereo' 66 | else: 67 | num_channels = f'{num_channels} Channels' 68 | return_dict['num_channels'] = num_channels 69 | return_dict['samplerate'] = int(f.samplerate) 70 | except Exception: 71 | pass 72 | finally: 73 | return return_dict 74 | 75 | @property 76 | def duration(self): 77 | return self.file_properties['duration'] 78 | 79 | @property 80 | def num_channels(self): 81 | return self.file_properties['num_channels'] 82 | 83 | @property 84 | def samplerate(self): 85 | return self.file_properties['samplerate'] 86 | 87 | @property 88 | def duration_str(self): 89 | return ':'.join(mmss(self.duration, string=True)) if self.duration else 'n/d' 90 | 91 | @property 92 | def canLoad(self): 93 | return self._canLoad if hasattr(self, '_canLoad') else True 94 | 95 | @canLoad.setter 96 | def canLoad(self, arg: bool): 97 | self._canLoad = arg 98 | 99 | @property 100 | def available(self): 101 | return bool(self.exists and self.duration >= MinAudioDuration and self.canLoad and self.samplerate >= 44100) 102 | 103 | @property 104 | def _default_dict(self): 105 | if self.isPinkNoise: 106 | return {'duration': 30, 'num_channels': 'Mono', 'samplerate': 44100} 107 | else: 108 | return {'duration': False, 'num_channels': None, 'samplerate': None} 109 | -------------------------------------------------------------------------------- /Model/AudioEngine/convert_audio.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | from pathlib import Path 19 | from pedalboard.io import AudioFile 20 | from Utilities.exceptions import InterruptedException 21 | 22 | 23 | def get_target_samplerate(source_sr: int or float, sr_mode: str): 24 | if sr_mode == 'original': 25 | ts = source_sr 26 | elif sr_mode == '48k': 27 | ts = 48000 28 | elif sr_mode == 'auto_div' and source_sr % 48000 == 0: 29 | ts = 48000 30 | else: 31 | ts = 44100 32 | return float(ts) 33 | 34 | 35 | def avoid_same_name(audiofile_path: str): 36 | result = Path(audiofile_path) 37 | while result.exists(): 38 | name_noext = str(result.with_suffix('')) 39 | digit_end = re.search(r'(?<=__)\d+$', name_noext) 40 | if digit_end is not None: 41 | digit_end = digit_end.group() 42 | name_noext = f"{name_noext[:-len(f'{digit_end}')]}{int(digit_end) + 1}" 43 | else: 44 | name_noext = f"{name_noext}__1" 45 | result = Path(name_noext + result.suffix) 46 | return str(result) 47 | 48 | 49 | def convert_audio(audiofile_path: str, source_samplerate: int or float, audio_format='WAVE', 50 | target_samplerate_mode='original', callback=None): 51 | # audio_format: WAVE / AIFF 52 | # sampling_rate_mode: original / 44.1k / 48k / auto_div 53 | def callback_out(): 54 | if callback is not None: 55 | callback(out_stat) 56 | 57 | if not Path(audiofile_path).is_file(): 58 | return None 59 | target_samplerate = get_target_samplerate(source_samplerate, target_samplerate_mode) 60 | out_ext_dict = {'WAVE': '.wav', 'AIFF': '.aiff', 'FLAC': '.flac'} 61 | out_ext = out_ext_dict[audio_format] 62 | output_path = Path(audiofile_path).with_suffix(out_ext) 63 | output_path_US = Path(audiofile_path).with_suffix(out_ext.upper()) 64 | if audiofile_path in (str(output_path), str(output_path_US)) and source_samplerate == target_samplerate: 65 | return None 66 | if source_samplerate == target_samplerate: 67 | input_af = AudioFile(audiofile_path, 'r') 68 | else: 69 | input_af = AudioFile(audiofile_path, 'r').resampled_to(target_samplerate) 70 | output_path = Path(f"{output_path.with_suffix('')} - Resampled{out_ext}") 71 | output_path = Path(avoid_same_name(str(output_path))) 72 | result = None 73 | out_stat = {'State': f'Converting "{Path(audiofile_path).name}" to "{output_path.name}"', 'Percent': 0} 74 | callback_out() 75 | with input_af as in_f: 76 | with AudioFile(str(output_path), 'w', target_samplerate, in_f.num_channels) as out_f: 77 | while in_f.tell() < in_f.frames: 78 | ch = in_f.read(int(target_samplerate)) 79 | out_f.write(ch) 80 | out_stat['Percent'] = int(out_f.frames / in_f.frames * 100) 81 | try: 82 | callback_out() 83 | except InterruptedException: 84 | out_f.close() 85 | output_path.unlink() 86 | return None 87 | if in_f.frames == out_f.frames: 88 | result = str(output_path) 89 | return result 90 | -------------------------------------------------------------------------------- /GUI/Modes/TestMode.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import QThreadPool 18 | from GUI.Modes.UniMode import UniMode 19 | from Model.AudioEngine.audio_to_buffer import a2b 20 | from GUI.Misc.adg_thread_run import ADGProc 21 | import numpy as np 22 | 23 | 24 | class TestMode(UniMode): 25 | _playbackStoppedEndedBlocked: bool 26 | ADGRun: ADGProc 27 | 28 | def __init__(self, parent): # parent: MainWindowContr 29 | super().__init__(parent) 30 | self.name = 'Test' 31 | self.currentDrillFreq = None 32 | self.view.SliceLenSpin.setEnabled(False) 33 | self.view.EQSetView.setEnabled(False) 34 | self.parent.EQContr.resetEQ() 35 | self.view.setActionNextExampleEnabled(False) 36 | self.setNextExampleVisible(True) 37 | self.setEQBandsOrderMenuEnabled(False) 38 | self.view.TransportPanelView.AudioSliderView.Cursor.hide() 39 | self.restart_test() 40 | self.parent.ADGC.setAudioDrillGen() 41 | self.nextDrill(fromStart=True) 42 | self.updateSliceRegion() 43 | self.view.TransportPanelView.AudioSliderView.SliceRegion.show() 44 | self.view.ExScoreInfo.show() 45 | self.parent.ExScore.showTestStatus() 46 | self.showAudioCursor() 47 | 48 | def generateDrill(self, fromStart=False): 49 | if self.parent.ADGen is None: 50 | return 51 | self.showProcessingSourceMessage() 52 | self.parent.TransportContr.updAudioToEqSettings(refreshAfter=False) 53 | self.ADGRun = ADGProc(self.parent.ADGen.output, audio_path=None, force_freq=None, 54 | fromStart=fromStart) 55 | self.ADGRun.signals.drillGenerated.connect(self._onDrillGenerated) 56 | threadPool = QThreadPool() 57 | threadPool.start(self.ADGRun) 58 | 59 | def _onDrillGenerated(self, freq: int or tuple, audio: np.ndarray): 60 | self.ADGRun.signals.drillGenerated.disconnect() 61 | self.currentDrillFreq = freq 62 | self.parent.CurrentAudio = a2b(audio, self.parent.ADGen.af_samplerate) 63 | self.parent.TransportContr.PlayerContr.loadCurrentAudio(play_after=True) 64 | self.parent.ExScore.nextEx() 65 | 66 | def nextDrill(self, fromStart=False, play_after=True, **kwargs): 67 | if self.parent.ADGen is None: 68 | return 69 | self.parent.TransportContr.PlayerContr.onStopTriggered(checkPlaybackState=True) 70 | self.view.setActionNextExampleEnabled(False) 71 | self.parent.EQContr.resetEQ() 72 | self.generateDrill(fromStart=fromStart) 73 | 74 | def acceptAnswer(self): 75 | eq_values = self.parent.EQContr.getEQValues() 76 | self.parent.ExScore.onAnswerAccepted(RightAnswer=self.currentDrillFreq, UserAnswer=eq_values) 77 | self.parent.EQContr.freezeEQ() 78 | self.parent.EQContr.highlightEQFreq(self.currentDrillFreq) 79 | self.view.setActionNextExampleEnabled(self.parent.ExScore.test_status == 'in progress') 80 | self.parent.ExScore.showTestStatus() 81 | 82 | def updateSliceRegion(self): 83 | self._updateSliceRegion() 84 | 85 | def restart_test(self): 86 | self.parent.ExScore.refresh() 87 | self.parent.ExScore.showTestStatus() 88 | -------------------------------------------------------------------------------- /GUI/Help/HelpActions.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import platform 18 | import webbrowser 19 | from PyQt6.QtCore import QObject, QTimer 20 | from PyQt6.QtGui import QTextDocument 21 | from GUI.Help.QuickHelpWin import QuickHelpWin 22 | from GUI.Misc.TextBrowserDocParameters import setParameters 23 | from definitions import ROOT_DIR 24 | from application import Settings 25 | from pathlib import Path 26 | from Utilities.str2bool import str2bool 27 | from Model.get_version import version 28 | 29 | 30 | class HelpActions(QObject): 31 | def __init__(self, mw_contr): 32 | super().__init__() 33 | self.mw_contr = mw_contr 34 | self.mw_view = mw_contr.mw_view 35 | self.mw_view.actionGetting_Started.triggered.connect(self.onGettingStarted_called) 36 | self.mw_view.actionOnline_Help.triggered.connect(self.onOnlineHelp_called) 37 | self.mw_view.actionVideo_Tutorial.triggered.connect(self.onVideoTutorial_called) 38 | self.mw_view.actionVideo_Tutorial_Rus.triggered.connect(self.onVideoTutorialRus_called) 39 | self.mw_view.actionReport_an_Issue.triggered.connect(self.onReportIssue_called) 40 | self.mw_view.actionGo_To_Source_Code.triggered.connect(self.onGoToSourceCode_called) 41 | self.mw_view.actionAsk_and_Discuss.triggered.connect(self.onAskAndDiscuss_called) 42 | self.mw_view.signals.MWFirstShown.connect(self.onAppStartup) 43 | self.GS_Win = QuickHelpWin(self.mw_view, title=f'Getting Started with EarQuiz Frequencies v{version()}', 44 | showagain_settings_path='MessageBoxes/ShowGettingStartedOnStartup') 45 | 46 | def onGettingStarted_called(self): 47 | if self.GS_Win.isVisible(): 48 | return 49 | content_path = Path(ROOT_DIR, 'GUI', 'Help', 'Data', 'get_started.md').absolute() 50 | with open(content_path, 'r', encoding='utf-8') as f: 51 | content = f.read() 52 | document = QTextDocument() 53 | document.setMarkdown(content) 54 | self.GS_Win.TextBr.setDocument(document) 55 | if platform.system() == 'Darwin': 56 | font_size = 16 57 | line_height = 120 58 | else: 59 | font_size = 13 60 | line_height = 110 61 | setParameters(self.GS_Win.TextBr, document, font_size=font_size, line_height=line_height) 62 | QTimer.singleShot(0, self.GS_Win.show) 63 | 64 | def onAppStartup(self): 65 | if str2bool(Settings.value('MessageBoxes/ShowGettingStartedOnStartup', True)): 66 | self.onGettingStarted_called() 67 | 68 | def onOnlineHelp_called(self): 69 | webbrowser.open('https://earquiz.org/manuals/earquiz-frequencies-help/') 70 | 71 | def onVideoTutorial_called(self): 72 | webbrowser.open('https://youtu.be/XOJai5Fdofw') 73 | 74 | def onVideoTutorialRus_called(self): 75 | webbrowser.open('https://youtu.be/pz-V5KNaBWU') 76 | 77 | def onReportIssue_called(self): 78 | webbrowser.open('https://github.com/Gdalik/EarQuiz_Frequencies/issues') 79 | 80 | def onAskAndDiscuss_called(self): 81 | webbrowser.open('https://github.com/Gdalik/EarQuiz_Frequencies/discussions') 82 | 83 | def onGoToSourceCode_called(self): 84 | webbrowser.open('https://github.com/Gdalik/EarQuiz_Frequencies') 85 | 86 | -------------------------------------------------------------------------------- /GUI/AudioProcSettings/audio_proc_settings_view.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from GUI.AudioProcSettings.audio_proc_settings_widget import Ui_AudioProcSettingsDialog 18 | from GUI.MainWindow.View.dark_theme import green_color 19 | from GUI.Misc.colorStr import colorStr 20 | from GUI.AudioProcSettings.eq_indicator_view import EqOnOffIndicatorView 21 | from PyQt6.QtWidgets import QWidget 22 | from PyQt6.QtCore import Qt 23 | from Utilities.common_calcs import eq_off_perc 24 | import Model.AudioEngine.audio_proc_settings as APS 25 | 26 | 27 | class AudioProcSettingsView(QWidget, Ui_AudioProcSettingsDialog): 28 | def __init__(self, mw_view): 29 | super().__init__(parent=mw_view) 30 | self.setupUi(self) 31 | self.EqIndView = EqOnOffIndicatorView(self.EQOnOffIndicator) 32 | self.updLabels() 33 | self.mw_view = mw_view 34 | Flags = Qt.WindowType(Qt.WindowType.Dialog | Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | 35 | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowStaysOnTopHint) 36 | self.setWindowFlags(Flags) 37 | self.EQOnTimeSlider.valueChanged.connect(self.on_EQOnTimeSlider_valueChanged) 38 | self.EQOnOffTransSpin.valueChanged.connect(self.setApplyButState) 39 | self.FadeInOutDurSpin.valueChanged.connect(self.setApplyButState) 40 | self.CloseBut.clicked.connect(self.onCloseBut_clicked) 41 | self.mw_view.actionEQ_Always_On_In_Test_Mode.setChecked(APS.getEQAlwaysOnInTest()) 42 | 43 | def onCloseBut_clicked(self): 44 | self.close() 45 | 46 | def updLabels(self): 47 | self.updEQOnOffPropLab() 48 | self.updEQOnTimeLab() 49 | 50 | def on_EQOnTimeSlider_valueChanged(self): 51 | self.updEQOnOffPropLab() 52 | self.setApplyButState() 53 | 54 | def setApplyButState(self): 55 | equalEQOnTimeValue = self.EQOnTimeSlider.value() == APS.getEQOnTimePerc() 56 | equalOnOffTransValue = self.EQOnOffTransSpin.value() == APS.getEQTransitionDur() * 1000 57 | equalFadeInOutValue = self.FadeInOutDurSpin.value() == APS.getExFadeInOutDur() * 1000 58 | self.ApplyBut.setEnabled(not(all((equalEQOnTimeValue, equalOnOffTransValue, equalFadeInOutValue)))) 59 | 60 | def updEQOnOffPropLab(self): 61 | EQOn_Perc = self.EQOnTimeSlider.value() 62 | EQOff_perc = eq_off_perc(EQOn_Perc) 63 | eq_off_text = colorStr('EQ Off', 'gray') 64 | eq_off_value_str = colorStr(f'({EQOff_perc}%)', 'gray') 65 | eq_on_value_str = colorStr(f'({EQOn_Perc}%)', green_color()) 66 | eq_off_lab_text = f'{eq_off_text}
{eq_off_value_str}
' 67 | self.EQOffLab.setText(eq_off_lab_text) 68 | self.EQOffLab2.setText(eq_off_lab_text) 69 | self.EQOnLab.setText(f'{self.eq_on_text}
{eq_on_value_str}
') 70 | self.EqIndView.update(EQOn_Perc) 71 | 72 | def updEQOnTimeLab(self): 73 | self.EQOnTimeLab.setText(f'{self.eq_on_text} Time:') 74 | 75 | def loadSettings(self): 76 | self.EQOnTimeSlider.setValue(APS.getEQOnTimePerc()) 77 | self.EQOnOffTransSpin.setValue(int(APS.getEQTransitionDur() * 1000)) 78 | self.FadeInOutDurSpin.setValue(int(APS.getExFadeInOutDur() * 1000)) 79 | 80 | def show(self): 81 | self.loadSettings() 82 | super(AudioProcSettingsView, self).show() 83 | 84 | @property 85 | def eq_on_text(self): 86 | return colorStr('EQ On', green_color()) 87 | -------------------------------------------------------------------------------- /Model/AudioEngine/preview_audio.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PyQt6.QtCore import pyqtSignal, QObject 18 | 19 | 20 | class PreviewAudioCrop(QObject): 21 | min_slice_length = 10 22 | max_slice_length = 30 23 | rangeChanged = pyqtSignal() 24 | sliceLengthChanged = pyqtSignal(int) 25 | 26 | def __init__(self, audiofile_length: int or float, starttime: int or float, endtime: int or float, 27 | slice_length=15, strictMode=False): 28 | super().__init__() 29 | self.source_length = audiofile_length 30 | self._strictMode = strictMode 31 | if strictMode: 32 | self._starttime = starttime 33 | self._endtime = endtime 34 | self._slice_length = slice_length 35 | else: 36 | self._starttime = max(0, starttime) 37 | self._endtime = max(0, endtime) 38 | self._starttime = min(self.starttime, self.endtime) 39 | self._endtime = max(self.starttime + self.min_slice_length, self.endtime) 40 | self._endtime = min(self._endtime, self.source_length) 41 | self._slice_length = max(slice_length, self.min_slice_length) 42 | self._slice_length = min(self._slice_length, self._endtime - self._starttime, self.max_slice_length) 43 | 44 | @property 45 | def starttime(self): 46 | return self._starttime 47 | 48 | @starttime.setter 49 | def starttime(self, value): 50 | old_value = self._starttime 51 | if self._strictMode: 52 | self._starttime = value 53 | else: 54 | self._starttime = max(value, 0) 55 | self._starttime = min(self._starttime, self.endtime - self.slice_length) 56 | if old_value != self._starttime: 57 | self.rangeChanged.emit() 58 | 59 | @property 60 | def endtime(self): 61 | return self._endtime 62 | 63 | @endtime.setter 64 | def endtime(self, value): 65 | old_value = self._endtime 66 | if self._strictMode: 67 | self._endtime = value 68 | else: 69 | self._endtime = max(self.starttime + self.slice_length, value) 70 | self._endtime = min(self._endtime, self.source_length) 71 | if old_value != self._endtime: 72 | self.rangeChanged.emit() 73 | self.slice_length = self.slice_length 74 | 75 | @property 76 | def slice_length(self): 77 | return self._slice_length 78 | 79 | @slice_length.setter 80 | def slice_length(self, value): 81 | old_value = self._slice_length 82 | if self._strictMode: 83 | self._slice_length = value 84 | self._slice_length = min(value, self.excerpt_length, self.max_slice_length) 85 | self._slice_length = max(self._slice_length, self.min_slice_length) 86 | slice_length_r = round(self._slice_length) 87 | self._slice_length = slice_length_r if self.excerpt_length // slice_length_r > 0 else old_value 88 | if old_value != self._slice_length: 89 | self.sliceLengthChanged.emit(self._slice_length) 90 | 91 | @property 92 | def excerpt_length(self): 93 | return self.endtime - self.starttime 94 | 95 | @property 96 | def slices_num(self): 97 | return int(self.excerpt_length // self.slice_length) 98 | 99 | @property 100 | def range(self): 101 | return self.starttime, self.endtime 102 | 103 | def setStrictModeActive(self, arg: bool): 104 | self._strictMode = arg 105 | -------------------------------------------------------------------------------- /GUI/PatternBox/patternbox_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | from Model.eq_patterns import EQPatterns 19 | from Utilities.exceptions import InterruptedException 20 | from application import Settings 21 | 22 | 23 | class PatternBoxContr(object): 24 | def __init__(self, mw_contr): 25 | self.EQPatterns = EQPatterns() 26 | self.mw_contr = mw_contr 27 | self.mw_view = mw_contr.mw_view 28 | self.mw_view.PatternBoxView.loadItems(self.getPatternNames()) 29 | self.PatternBox = self.mw_view.PatternBox 30 | self.NextPatternBut = self.mw_view.NextPatternBut 31 | self.loadStoredPattern() 32 | self.onPatternBoxIndexChanged() 33 | self.PatternBox.currentIndexChanged.connect(self.onPatternBoxIndexChanged) 34 | self.NextPatternBut.clicked.connect(self.onNextPatternBut_clicked) 35 | self._nextPatternButEnable() 36 | 37 | def onPatternBoxIndexChanged(self, index=None): 38 | index = self.PatternBox.currentIndex() if index is None else index 39 | self.mw_contr.EQContr.setEQMode(mode_num=index + 1) 40 | self._nextPatternButEnable() 41 | if not self.mw_view.actionLockEQSettings.isChecked(): 42 | self.mw_contr.EQSetContr.refreshSet() 43 | self.setExGenToPattern() 44 | self._restartExamples() 45 | self.saveLastPattern() 46 | 47 | def _restartExamples(self): 48 | with contextlib.suppress(AttributeError): 49 | if self.mw_contr.CurrentMode.name == 'Test': 50 | self.mw_contr.CurrentMode.restart_test() 51 | elif self.mw_contr.CurrentMode.name == 'Uni': 52 | self.mw_view.actionPreview_Mode.setChecked(True) 53 | return 54 | try: 55 | self.mw_contr.CurrentMode.nextDrill(fromStart=True, play_after=True) 56 | except InterruptedException: 57 | self.mw_view.actionPreview_Mode.setChecked(True) 58 | 59 | def setExGenToPattern(self): 60 | if not hasattr(self.mw_contr, 'SourceAudio') or self.mw_contr.SourceAudio is None: 61 | return 62 | if not hasattr(self.mw_contr, 'ADGen') or self.mw_contr.ADGen is None: 63 | return 64 | eq_pattern = self.mw_contr.EQContr.EQpattern 65 | exgen_order = self.mw_contr.freqOrder() 66 | self.mw_contr.ADGen.resetExGen(self.mw_contr.EQContr.getAvailableFreq(), 67 | boost_cut=eq_pattern['EQ_boost_cut'], 68 | DualBandMode=eq_pattern['DualBandMode'], 69 | order=exgen_order, 70 | boost_cut_priority=self.mw_contr.boostCutPriority, 71 | disableAdjacent=eq_pattern['DisableAdjacentFiltersMode'], 72 | inf_cycle=True) 73 | 74 | def onNextPatternBut_clicked(self): 75 | PBindex = self.PatternBox.currentIndex() 76 | max_index = self.PatternBox.count() - 1 77 | index = PBindex + 1 if PBindex < max_index else max_index 78 | self.PatternBox.setCurrentIndex(index) 79 | 80 | def _nextPatternButEnable(self): 81 | self.NextPatternBut.setEnabled(self.PatternBox.currentIndex() < 82 | self.PatternBox.count() - 1) 83 | 84 | def getPatternNames(self): 85 | return [mode['Name'] for mode in self.EQPatterns.List] 86 | 87 | def saveLastPattern(self): 88 | Settings.setValue('LastStuff/EQ_Pattern', self.PatternBox.currentText()) 89 | 90 | def loadStoredPattern(self): 91 | stored_value = Settings.value('LastStuff/EQ_Pattern', self.PatternBox.itemText(0)) 92 | self.PatternBox.setCurrentText(stored_value) 93 | -------------------------------------------------------------------------------- /GUI/EQSettings/eqset_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | from pathlib import PurePath 19 | 20 | import definitions 21 | from GUI.Misc.tracked_proc import ProcTrackControl 22 | from Utilities.Q_extract import Qextr 23 | from Utilities.exceptions import InterruptedException 24 | from application import Settings 25 | 26 | 27 | class EQSetContr: # parent: MainWindowContr 28 | def __init__(self, parent): 29 | self.parent = parent 30 | self.EQSetView = parent.mw_view.EQSetView 31 | self.BWQPresets = self.getPresetNames() 32 | self.EQSetView.refreshBWQList(self.BWQPresets) 33 | self.ResetEQBut = parent.mw_view.ResetEQBut 34 | self.ResetEQBut.clicked.connect(self.on_ResetClicked) 35 | self.parent.mw_view.actionLockEQSettings.triggered.connect(self.onActionLockEQSettings_trig) 36 | self.restoreEQSettings() 37 | 38 | @property 39 | def EQpattern(self): 40 | return self.parent.EQContr.EQpattern 41 | 42 | def refreshSet(self): 43 | EQpattern = self.EQpattern 44 | if EQpattern is None: 45 | return 46 | BW_Q = EQpattern['BW_Q'] 47 | self.BWQPresets = self.getPresetNames() 48 | if BW_Q not in self.BWQPresets: 49 | self._addCustomBWQPreset(BW_Q) 50 | self.EQSetView.refreshBWQList(self.BWQPresets) 51 | self.EQSetView.update(EQpattern['Gain_depth'], BW_Q) 52 | 53 | @staticmethod 54 | def getPresetNames(): 55 | with open(PurePath(definitions.ROOT_DIR, 'Model', 'Data', 'bw_q_patterns.json')) as f: 56 | preset_list = json.load(f) 57 | return preset_list 58 | 59 | def _addCustomBWQPreset(self, BW_Q: str): 60 | self.BWQPresets.append(BW_Q) 61 | self.BWQPresets.sort(key=lambda Q: Qextr(Q)) 62 | 63 | def on_ResetClicked(self): 64 | self.EQSetView.update(self.EQpattern['Gain_depth'], self.EQpattern['BW_Q']) 65 | 66 | def setGainDepth(self, value: int, raiseInterruptedException=True): 67 | if self.parent.ADGen is None: 68 | return 69 | old_ADGen_gain_depth = self.parent.ADGen.gain_depth() 70 | ADG_gain_upd = ProcTrackControl(self.parent.ADGen.setGain_depth, args=[value]) 71 | if not ADG_gain_upd.exec(): 72 | self.parent.isErrorInProcess(ADG_gain_upd) 73 | self.parent.ADGen.setGain_depth(old_ADGen_gain_depth, normalize_audio=False) 74 | if raiseInterruptedException: 75 | raise InterruptedException 76 | self.EQSetView.update_gain_depth(self.parent.ADGen.gain_depth()) 77 | return False 78 | return True 79 | 80 | def updADGenQ(self): 81 | if self.parent.ADGen is None: 82 | return 83 | self.parent.ADGen.Q = Qextr(self.EQSetView.BWBox.currentText()) 84 | 85 | def onActionLockEQSettings_trig(self): 86 | self.saveEQSettings() 87 | 88 | def saveEQSettings(self): 89 | if self.parent.mw_view.actionLockEQSettings.isChecked(): 90 | Settings.setValue('LastStuff/EQSettingsLocked', {'GainDepth': self.EQSetView.GainRangeSpin.value(), 91 | 'BW': self.EQSetView.BWBox.currentText()}) 92 | else: 93 | Settings.setValue('LastStuff/EQSettingsLocked', None) 94 | 95 | def restoreEQSettings(self): 96 | values = Settings.value('LastStuff/EQSettingsLocked', None) 97 | if values is None: 98 | self.parent.mw_view.actionLockEQSettings.setChecked(False) 99 | return 100 | self.parent.mw_view.actionLockEQSettings.setChecked(True) 101 | self.EQSetView.update(int(values['GainDepth']), values['BW']) 102 | -------------------------------------------------------------------------------- /GUI/MainWindow/Contr/adgen_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from GUI.Misc.tracked_proc import ProcTrackControl 18 | from Model.audiodrill_gen import AudioDrillGen 19 | import Model.AudioEngine.audio_proc_settings as APS 20 | from Utilities.Q_extract import Qextr 21 | 22 | 23 | class ADGenContr: 24 | def __init__(self, parent): # parent: MainWindowContr 25 | self.parent = parent 26 | 27 | def setAudioDrillGen(self, resetExGen=True): 28 | if self.parent.ADGen is None and self.parent.SourceAudio is not None \ 29 | and (self.parent.SourceAudio.isPinkNoise or self.parent.LoadedFileHash): 30 | self._createADGen() 31 | self._adjustADGenOrderToMode() 32 | elif self.parent.ADGen is not None: 33 | self._adjustADGenCropRange() 34 | if resetExGen: 35 | self.parent.PatternBoxContr.setExGenToPattern() 36 | 37 | def _createADGen(self): 38 | EQP = self.parent.EQContr.EQpattern 39 | SR = self.parent.SourceRange 40 | SA = self.parent.SourceAudio 41 | ADG = ProcTrackControl(AudioDrillGen, args=[self.parent.EQContr.getAvailableFreq()], 42 | kwargs={'boost_cut': EQP['EQ_boost_cut'], 43 | 'DualBandMode': EQP['DualBandMode'], 44 | 'audio_source_path': SA.path, 45 | 'starttime': SR.starttime, 46 | 'endtime': SR.endtime, 47 | 'drill_length': SR.slice_length, 48 | 'gain_depth': self.parent.EQSetContr.EQSetView.GainRangeSpin.value(), 49 | 'Q': Qextr(self.parent.EQSetContr.EQSetView.BWBox.currentText()), 50 | 'proc_t_perc': APS.getEQOnTimePerc(), 51 | 'order': self.parent.freqOrder(), 52 | 'boost_cut_priority': self.parent.boostCutPriority, 53 | 'disableAdjacent': EQP['DisableAdjacentFiltersMode']}) 54 | ADG.exec() 55 | self.parent.isErrorInProcess(ADG) 56 | self.parent.ADGen = ADG.return_obj or None 57 | if self.parent.ADGen is not None: 58 | self.parent.ADGen.audiochunk.signals.showNormalizationLevel.connect( 59 | self.parent.mw_view.status.showNormalization) 60 | 61 | def _adjustADGenCropRange(self): 62 | SR = self.parent.SourceRange 63 | self.parent.ADGen.audiochunk.setStrictModeActive(True) 64 | self.parent.ADGen.audiochunk.starttime = SR.starttime 65 | self.parent.ADGen.audiochunk.endtime = SR.endtime 66 | self.parent.ADGen.audiochunk.slice_length = SR.slice_length 67 | self.parent.ADGen.audiochunk.setStrictModeActive(False) 68 | action = self.parent.ADGen.audiochunk.checkActionNeeded() 69 | if action is None: 70 | return 71 | if action == 'reset': 72 | self.parent.ADGen.setGain_depth(self.parent.EQSetContr.EQSetView.GainRangeSpin.value(), 73 | normalize_audio=False) 74 | ADG_upd = ProcTrackControl(self.parent.ADGen.audiochunk.update, args=[action]) 75 | if not ADG_upd.exec(): 76 | self.parent.ADGen = None 77 | if ADG_upd.error: 78 | self.parent.mw_view.error_msg(ADG_upd.error) 79 | self.parent.ADGen = None 80 | else: 81 | self.parent.ADGen.audiochunk.update(mode=action) 82 | 83 | def _adjustADGenOrderToMode(self): 84 | if self.parent.ADGen is None: 85 | return 86 | self.parent.ADGen.order = self.parent.freqOrder() 87 | -------------------------------------------------------------------------------- /GUI/MainWindow/Contr/sourcerange_contr.py: -------------------------------------------------------------------------------- 1 | # EarQuiz Frequencies. Software for technical ear training on equalization. 2 | # Copyright (C) 2023-2025, Gdaliy Garmiza. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import contextlib 18 | from Model.AudioEngine.preview_audio import PreviewAudioCrop 19 | from Model.calc import optimal_range_length 20 | from Model.sourcerange_manager import SourceRangeManager 21 | from definitions import SineWaveCalibrationFilename 22 | 23 | 24 | class SourceRangeContr: # parent: MainWindowContr 25 | def __init__(self, parent): 26 | self.parent = parent 27 | self.TransportContr = self.parent.TransportContr 28 | self.mw_view = self.parent.mw_view 29 | 30 | def setInitSourceRangeView(self): 31 | self.disconnectSourceRangeSig() 32 | self.autoSetSourceRange() 33 | self.mw_view.TransportPanelView.CropRegionTstr.noAudioState(False) 34 | self.parent.SourceRange.rangeChanged.connect(self.TransportContr.onSourceRangeChanged) 35 | self.parent.SourceRange.sliceLengthChanged.connect(self.TransportContr.onSliceLenChanged) 36 | 37 | def autoSetSourceRange(self, reset=True): 38 | srm = SourceRangeManager() 39 | range_params = srm.get(self.parent.LoadedFileHash) if self.parent.LoadedFileHash is not None else None 40 | if range_params is None: 41 | self.setOptimalSourceRange(reset=reset) 42 | return 43 | self.setSourceRange(range_params, reset=reset) 44 | 45 | def setSourceRange(self, params, reset=True): 46 | if reset: 47 | self.parent.SourceRange = PreviewAudioCrop(self.parent.SourceAudio.duration, params[0], 48 | params[1], 49 | params[2]) 50 | else: 51 | self.parent.SourceRange.setStrictModeActive(True) 52 | self.parent.SourceRange.starttime, self.parent.SourceRange.endtime, _ = params 53 | self.parent.SourceRange.setStrictModeActive(False) 54 | 55 | def setOptimalSourceRange(self, reset=True): 56 | range_params = self._getOptSourceRangeParameters(reset=reset) 57 | if reset or self.parent.SourceRange is None: 58 | self.parent.SourceRange = PreviewAudioCrop(self.parent.SourceAudio.duration, range_params[0], 59 | range_params[1], range_params[2]) 60 | else: 61 | self.parent.SourceRange.setStrictModeActive(True) 62 | self.parent.SourceRange.starttime = 0 63 | self.parent.SourceRange.endtime = range_params[1] 64 | self.parent.SourceRange.setStrictModeActive(False) 65 | self.parent.SourceRange.slice_length = range_params[2] 66 | 67 | def _getOptSourceRangeParameters(self, reset=True): 68 | duration = self.parent.SourceAudio.duration 69 | if self.parent.SourceAudio.name == SineWaveCalibrationFilename: 70 | slice_length = 10 71 | elif reset: 72 | slice_length = self.parent.CurrentSourceMode.default_slice_length 73 | else: 74 | slice_length = self.mw_view.TransportPanelView.SliceLenSpin.value() 75 | slice_length = int(min(duration, slice_length)) 76 | opt_length = optimal_range_length(duration, slice_length) 77 | return 0, opt_length, slice_length 78 | 79 | def disconnectSourceRangeSig(self): 80 | with contextlib.suppress(AttributeError, TypeError): 81 | self.parent.SourceRange.rangeChanged.disconnect(self.TransportContr.onSourceRangeChanged) 82 | self.parent.SourceRange.sliceLengthChanged.disconnect(self.TransportContr.onSliceLenChanged) 83 | 84 | def savePrevSourceAudioRange(self): 85 | if self.parent.LoadedFileHash is None or self.parent.SourceAudio is None or self.parent.SourceRange is None: 86 | return 87 | srm = SourceRangeManager() 88 | srm.save(self.parent.LoadedFileHash, self.parent.SourceAudio.name, self.parent.SourceRange) 89 | --------------------------------------------------------------------------------