From e085b2ef6b29b0d8bf9ffbaca52a8afdf72f9ea0 Mon Sep 17 00:00:00 2001 From: Ales Nosek Date: Sun, 12 Feb 2012 18:43:00 +0100 Subject: [PATCH] Import source code --- .autotools | 42 + .cproject | 55 + .gitignore | 7 + .project | 84 + .pydevproject | 11 + .settings/org.eclipse.cdt.codan.core.prefs | 66 + COPYING | 339 +++ Makefile.in | 151 ++ README | 0 README.md | 34 + autogen.sh | 3 + configure.ac | 75 + src/main/c/linuxband-player.c | 888 ++++++ src/main/c/midi.c | 305 +++ src/main/c/midi.h | 57 + src/main/c/remote_control.c | 516 ++++ src/main/c/remote_control.h | 42 + src/main/config/default.mma | 11 + src/main/config/linuxband.rc.in | 15 + src/main/glade/gui.glade | 2385 +++++++++++++++++ src/main/python/linuxband.py.in | 77 + src/main/python/linuxband/__init__.py | 17 + src/main/python/linuxband/config.py | 143 + src/main/python/linuxband/glob.py | 63 + src/main/python/linuxband/gui/__init__.py | 17 + src/main/python/linuxband/gui/about_dialog.py | 53 + .../python/linuxband/gui/chord_entries.py | 186 ++ src/main/python/linuxband/gui/chord_sheet.py | 567 ++++ src/main/python/linuxband/gui/common.py | 30 + .../python/linuxband/gui/events/__init__.py | 17 + .../python/linuxband/gui/events/groove.py | 128 + .../python/linuxband/gui/events/repeat.py | 31 + .../python/linuxband/gui/events/repeat_end.py | 73 + .../linuxband/gui/events/repeat_ending.py | 74 + src/main/python/linuxband/gui/events/tempo.py | 71 + src/main/python/linuxband/gui/events_bar.py | 334 +++ src/main/python/linuxband/gui/gui.py | 548 ++++ src/main/python/linuxband/gui/gui_logger.py | 84 + src/main/python/linuxband/gui/preferences.py | 65 + .../linuxband/gui/save_button_status.py | 35 + .../python/linuxband/gui/source_editor.py | 175 ++ src/main/python/linuxband/logger.py | 27 + src/main/python/linuxband/midi/__init__.py | 17 + src/main/python/linuxband/midi/midi_player.py | 229 ++ src/main/python/linuxband/midi/mma2smf.py | 130 + src/main/python/linuxband/mma/__init__.py | 17 + src/main/python/linuxband/mma/bar_chords.py | 132 + src/main/python/linuxband/mma/bar_info.py | 206 ++ src/main/python/linuxband/mma/chord_table.py | 458 ++++ src/main/python/linuxband/mma/grooves.py | 166 ++ src/main/python/linuxband/mma/parse.py | 652 +++++ src/main/python/linuxband/mma/song.py | 132 + src/main/python/linuxband/mma/song_data.py | 174 ++ src/main/resources/error-pointer.png | Bin 0 -> 710 bytes src/main/resources/line-pointer.png | Bin 0 -> 601 bytes .../python/linuxband/mma/test_bar_chords.py | 73 + src/test/python/linuxband/mma/test_parse.py | 80 + 57 files changed, 10367 insertions(+) create mode 100644 .autotools create mode 100644 .cproject create mode 100644 .gitignore create mode 100644 .project create mode 100644 .pydevproject create mode 100644 .settings/org.eclipse.cdt.codan.core.prefs create mode 100644 COPYING create mode 100644 Makefile.in delete mode 100644 README create mode 100644 README.md create mode 100755 autogen.sh create mode 100644 configure.ac create mode 100644 src/main/c/linuxband-player.c create mode 100644 src/main/c/midi.c create mode 100644 src/main/c/midi.h create mode 100644 src/main/c/remote_control.c create mode 100644 src/main/c/remote_control.h create mode 100644 src/main/config/default.mma create mode 100644 src/main/config/linuxband.rc.in create mode 100644 src/main/glade/gui.glade create mode 100755 src/main/python/linuxband.py.in create mode 100644 src/main/python/linuxband/__init__.py create mode 100644 src/main/python/linuxband/config.py create mode 100644 src/main/python/linuxband/glob.py create mode 100644 src/main/python/linuxband/gui/__init__.py create mode 100644 src/main/python/linuxband/gui/about_dialog.py create mode 100644 src/main/python/linuxband/gui/chord_entries.py create mode 100644 src/main/python/linuxband/gui/chord_sheet.py create mode 100644 src/main/python/linuxband/gui/common.py create mode 100644 src/main/python/linuxband/gui/events/__init__.py create mode 100644 src/main/python/linuxband/gui/events/groove.py create mode 100644 src/main/python/linuxband/gui/events/repeat.py create mode 100644 src/main/python/linuxband/gui/events/repeat_end.py create mode 100644 src/main/python/linuxband/gui/events/repeat_ending.py create mode 100644 src/main/python/linuxband/gui/events/tempo.py create mode 100644 src/main/python/linuxband/gui/events_bar.py create mode 100644 src/main/python/linuxband/gui/gui.py create mode 100644 src/main/python/linuxband/gui/gui_logger.py create mode 100644 src/main/python/linuxband/gui/preferences.py create mode 100644 src/main/python/linuxband/gui/save_button_status.py create mode 100644 src/main/python/linuxband/gui/source_editor.py create mode 100644 src/main/python/linuxband/logger.py create mode 100644 src/main/python/linuxband/midi/__init__.py create mode 100644 src/main/python/linuxband/midi/midi_player.py create mode 100644 src/main/python/linuxband/midi/mma2smf.py create mode 100644 src/main/python/linuxband/mma/__init__.py create mode 100644 src/main/python/linuxband/mma/bar_chords.py create mode 100644 src/main/python/linuxband/mma/bar_info.py create mode 100644 src/main/python/linuxband/mma/chord_table.py create mode 100644 src/main/python/linuxband/mma/grooves.py create mode 100644 src/main/python/linuxband/mma/parse.py create mode 100644 src/main/python/linuxband/mma/song.py create mode 100644 src/main/python/linuxband/mma/song_data.py create mode 100644 src/main/resources/error-pointer.png create mode 100644 src/main/resources/line-pointer.png create mode 100644 src/test/python/linuxband/mma/test_bar_chords.py create mode 100644 src/test/python/linuxband/mma/test_parse.py diff --git a/.autotools b/.autotools new file mode 100644 index 0000000..f48f38c --- /dev/null +++ b/.autotools @@ -0,0 +1,42 @@ + + + + + diff --git a/.cproject b/.cproject new file mode 100644 index 0000000..1083703 --- /dev/null +++ b/.cproject @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53ba779 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +aclocal.m4 +config.h.in +config.status +configure +install-sh +Makefile diff --git a/.project b/.project new file mode 100644 index 0000000..c955d53 --- /dev/null +++ b/.project @@ -0,0 +1,84 @@ + + + linuxband + + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.linuxtools.cdt.autotools.core.genmakebuilderV2 + + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + ?name? + + + + org.eclipse.cdt.make.core.append_environment + true + + + org.eclipse.cdt.make.core.buildArguments + + + + org.eclipse.cdt.make.core.buildCommand + make + + + org.eclipse.cdt.make.core.contents + org.eclipse.cdt.make.core.activeConfigSettings + + + org.eclipse.cdt.make.core.enableAutoBuild + false + + + org.eclipse.cdt.make.core.enableCleanBuild + true + + + org.eclipse.cdt.make.core.enableFullBuild + true + + + org.eclipse.cdt.make.core.stopOnError + true + + + org.eclipse.cdt.make.core.useDefaultBuildCmd + true + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + org.python.pydev.PyDevBuilder + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + org.eclipse.linuxtools.cdt.autotools.core.autotoolsNatureV2 + org.eclipse.wst.common.project.facet.core.nature + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..36d917d --- /dev/null +++ b/.pydevproject @@ -0,0 +1,11 @@ + + + + + +/linuxband/src/main/python +/linuxband/src/test/python + +python 2.5 +Default + diff --git a/.settings/org.eclipse.cdt.codan.core.prefs b/.settings/org.eclipse.cdt.codan.core.prefs new file mode 100644 index 0000000..438a5e3 --- /dev/null +++ b/.settings/org.eclipse.cdt.codan.core.prefs @@ -0,0 +1,66 @@ +#Sat Feb 04 09:45:09 CET 2012 +eclipse.preferences.version=1 +org.eclipse.cdt.codan.checkers.errnoreturn=Warning +org.eclipse.cdt.codan.checkers.errnoreturn.params={implicit\=>false} +org.eclipse.cdt.codan.checkers.errreturnvalue=Error +org.eclipse.cdt.codan.checkers.errreturnvalue.params={} +org.eclipse.cdt.codan.checkers.noreturn=Error +org.eclipse.cdt.codan.checkers.noreturn.params={implicit\=>false} +org.eclipse.cdt.codan.internal.checkers.AbstractClassCreation=Error +org.eclipse.cdt.codan.internal.checkers.AbstractClassCreation.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.AmbiguousProblem=Error +org.eclipse.cdt.codan.internal.checkers.AmbiguousProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.AssignmentInConditionProblem=Warning +org.eclipse.cdt.codan.internal.checkers.AssignmentInConditionProblem.params={} +org.eclipse.cdt.codan.internal.checkers.AssignmentToItselfProblem=Error +org.eclipse.cdt.codan.internal.checkers.AssignmentToItselfProblem.params={} +org.eclipse.cdt.codan.internal.checkers.CaseBreakProblem=Warning +org.eclipse.cdt.codan.internal.checkers.CaseBreakProblem.params={no_break_comment\=>"no break",last_case_param\=>true,empty_case_param\=>false} +org.eclipse.cdt.codan.internal.checkers.CatchByReference=Warning +org.eclipse.cdt.codan.internal.checkers.CatchByReference.params={unknown\=>false,exceptions\=>()} +org.eclipse.cdt.codan.internal.checkers.CircularReferenceProblem=Error +org.eclipse.cdt.codan.internal.checkers.CircularReferenceProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.FieldResolutionProblem=Error +org.eclipse.cdt.codan.internal.checkers.FieldResolutionProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.FunctionResolutionProblem=Error +org.eclipse.cdt.codan.internal.checkers.FunctionResolutionProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.InvalidArguments=Error +org.eclipse.cdt.codan.internal.checkers.InvalidArguments.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.InvalidTemplateArgumentsProblem=Error +org.eclipse.cdt.codan.internal.checkers.InvalidTemplateArgumentsProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.LabelStatementNotFoundProblem=Error +org.eclipse.cdt.codan.internal.checkers.LabelStatementNotFoundProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.MemberDeclarationNotFoundProblem=Error +org.eclipse.cdt.codan.internal.checkers.MemberDeclarationNotFoundProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.MethodResolutionProblem=Error +org.eclipse.cdt.codan.internal.checkers.MethodResolutionProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.NamingConventionFunctionChecker=-Info +org.eclipse.cdt.codan.internal.checkers.NamingConventionFunctionChecker.params={pattern\=>"^[a-z]",macro\=>true,exceptions\=>()} +org.eclipse.cdt.codan.internal.checkers.NonVirtualDestructorProblem=Warning +org.eclipse.cdt.codan.internal.checkers.NonVirtualDestructorProblem.params={} +org.eclipse.cdt.codan.internal.checkers.OverloadProblem=Error +org.eclipse.cdt.codan.internal.checkers.OverloadProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.RedeclarationProblem=Error +org.eclipse.cdt.codan.internal.checkers.RedeclarationProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.RedefinitionProblem=Error +org.eclipse.cdt.codan.internal.checkers.RedefinitionProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.ReturnStyleProblem=-Warning +org.eclipse.cdt.codan.internal.checkers.ReturnStyleProblem.params={} +org.eclipse.cdt.codan.internal.checkers.ScanfFormatStringSecurityProblem=-Warning +org.eclipse.cdt.codan.internal.checkers.ScanfFormatStringSecurityProblem.params={} +org.eclipse.cdt.codan.internal.checkers.StatementHasNoEffectProblem=Warning +org.eclipse.cdt.codan.internal.checkers.StatementHasNoEffectProblem.params={macro\=>true,exceptions\=>()} +org.eclipse.cdt.codan.internal.checkers.SuggestedParenthesisProblem=Warning +org.eclipse.cdt.codan.internal.checkers.SuggestedParenthesisProblem.params={paramNot\=>false} +org.eclipse.cdt.codan.internal.checkers.SuspiciousSemicolonProblem=Warning +org.eclipse.cdt.codan.internal.checkers.SuspiciousSemicolonProblem.params={else\=>false,afterelse\=>false} +org.eclipse.cdt.codan.internal.checkers.TypeResolutionProblem=Error +org.eclipse.cdt.codan.internal.checkers.TypeResolutionProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} +org.eclipse.cdt.codan.internal.checkers.UnusedFunctionDeclarationProblem=Warning +org.eclipse.cdt.codan.internal.checkers.UnusedFunctionDeclarationProblem.params={macro\=>true} +org.eclipse.cdt.codan.internal.checkers.UnusedStaticFunctionProblem=Warning +org.eclipse.cdt.codan.internal.checkers.UnusedStaticFunctionProblem.params={macro\=>true} +org.eclipse.cdt.codan.internal.checkers.UnusedVariableDeclarationProblem=Warning +org.eclipse.cdt.codan.internal.checkers.UnusedVariableDeclarationProblem.params={macro\=>true,exceptions\=>("@(\#)","$Id")} +org.eclipse.cdt.codan.internal.checkers.VariableResolutionProblem=Error +org.eclipse.cdt.codan.internal.checkers.VariableResolutionProblem.params={launchModes\=>{RUN_ON_FULL_BUILD\=>false,RUN_ON_INC_BUILD\=>false,RUN_AS_YOU_TYPE\=>true,RUN_ON_DEMAND\=>true}} diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Makefile.in b/Makefile.in new file mode 100644 index 0000000..ce0e05a --- /dev/null +++ b/Makefile.in @@ -0,0 +1,151 @@ +# Package-specific substitution variables +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_TITLE = Linux Band +PACKAGE_COPYRIGHT = (c) 2012 Ales Nosek +PACKAGE_FULL_NAME = ${PACKAGE_NAME}-${PACKAGE_VERSION} + +# Prefix-specific substitution variables +prefix = @prefix@ +exec_prefix = @exec_prefix@ +bindir = @bindir@ +datarootdir = @datarootdir@ +pkgdatadir = @pkgdatadir@ +pkglibdir = @pkglibdir@ + +# Compiler +GLIB_CFLAGS = @GLIB_CFLAGS@ +GLIB_LIBS = @GLIB_LIBS@ +GTHREAD_CFLAGS = @GTHREAD_CFLAGS@ +GTHREAD_LIBS = @GTHREAD_LIBS@ +JACK_CFLAGS = @JACK_CFLAGS@ +JACK_LIBS = @JACK_LIBS@ +SMF_CFLAGS = @SMF_CFLAGS@ +SMF_LIBS = @SMF_LIBS@ + +# Utilities +INSTALL = @INSTALL@ + +EXTRA_CFLAGS = ${GLIB_CFLAGS} ${GTHREAD_CFLAGS} ${JACK_CFLAGS} ${SMF_CFLAGS} -I. +EXTRA_LIBS = ${GLIB_LIBS} ${GTHREAD_LIBS} ${JACK_LIBS} ${SMF_LIBS} + +PY_SOURCE_DIR = src/main/python +C_SOURCE_DIR = src/main/c +CONFIG_DIR = src/main/config +GLADE_DIR = src/main/glade +RESOURCES_DIR = src/main/resources +PY_TEST_DIR = src/test/python +TARGET_DIR = target +DIST_DIR = target +DIST_CHECK_DIR = target/dist-check + +linuxband_player_SOURCES = $(addprefix ${C_SOURCE_DIR}/,linuxband-player.c midi.c remote_control.c) +linuxband_player_HEADERS = $(addprefix ${C_SOURCE_DIR}/,midi.h remote_control.h) + +CLEAN_FILES = ${TARGET_DIR}/linuxband-player ${TARGET_DIR}/linuxband +DISTCLEAN_FILES = ${TARGET_DIR} Makefile config.status config.cache config.log config.h +MAINTAINERCLEAN_FILES = ${DISTCLEAN_FILES} aclocal.m4 autom4te.cache configure config.h.in install-sh + +AUTOCONF_FILES = aclocal.m4 autogen.sh autom4te.cache config.h.in configure configure.ac install-sh +SOURCE_FILES = src +MISC_FILES = Makefile.in COPYING README.md +DIST_FILES = ${AUTOCONF_FILES} ${SOURCE_FILES} ${MISC_FILES} + +.SUFFIXES: # disable built-in rules + +all: ${TARGET_DIR}/linuxband-player ${TARGET_DIR}/linuxband ${TARGET_DIR}/linuxband.rc + +${TARGET_DIR}/linuxband-player: ${linuxband_player_SOURCES} ${linuxband_player_HEADERS} + @mkdir -p ${@D} + ${CC} -o $@ ${CFLAGS} ${EXTRA_CFLAGS} ${CPPFLAGS} ${LDFLAGS} ${EXTRA_LIBS} ${linuxband_player_SOURCES} + +${TARGET_DIR}/linuxband: ${PY_SOURCE_DIR}/linuxband.py.in + @mkdir -p ${@D} + sed \ + -e 's#[@]PACKAGE_NAME@#${PACKAGE_NAME}#g' \ + -e 's#[@]PACKAGE_VERSION@#${PACKAGE_VERSION}#g' \ + -e 's#[@]PACKAGE_BUGREPORT@#${PACKAGE_BUGREPORT}#g' \ + -e 's#[@]PACKAGE_URL@#${PACKAGE_URL}#g' \ + -e 's#[@]PACKAGE_TITLE@#${PACKAGE_TITLE}#g' \ + -e 's#[@]PACKAGE_COPYRIGHT@#${PACKAGE_COPYRIGHT}#g' \ + -e 's#[@]pkgdatadir@#${pkgdatadir}#g' \ + -e 's#[@]pkglibdir@#${pkglibdir}#g' \ + < "$<" > "$@" + +${TARGET_DIR}/linuxband.rc: ${CONFIG_DIR}/linuxband.rc.in + @mkdir -p ${@D} + sed \ + -e 's#[@]PACKAGE_VERSION@#${PACKAGE_VERSION}#g' \ + -e 's#[@]pkgdatadir@#${pkgdatadir}#g' \ + < "$<" > "$@" + +check: + export PYTHONPATH=${PY_SOURCE_DIR}:${PY_TEST_DIR}; \ + python ${PY_TEST_DIR}/linuxband/mma/test_bar_chords.py + +install: all + ${INSTALL} -d ${DESTDIR}${bindir} + ${INSTALL} -d ${DESTDIR}${pkglibdir} + ${INSTALL} -d ${DESTDIR}${pkgdatadir} + ${INSTALL} -t ${DESTDIR}${bindir} ${TARGET_DIR}/linuxband + ${INSTALL} -t ${DESTDIR}${pkglibdir} ${TARGET_DIR}/linuxband-player + ${INSTALL} -t ${DESTDIR}${pkgdatadir} ${TARGET_DIR}/linuxband.rc + PY_SOURCE=$$(cd ${PY_SOURCE_DIR} && find . -name '*.py'); \ + for F in $${PY_SOURCE}; do \ + ${INSTALL} -D ${PY_SOURCE_DIR}/$$F ${DESTDIR}${pkgdatadir}/$$F; done + ${INSTALL} -t ${DESTDIR}${pkgdatadir} ${CONFIG_DIR}/default.mma + ${INSTALL} -t ${DESTDIR}${pkgdatadir} ${GLADE_DIR}/gui.glade + ${INSTALL} COPYING ${DESTDIR}${pkgdatadir}/license.txt + ${INSTALL} -t ${DESTDIR}${pkgdatadir} ${RESOURCES_DIR}/* + +uninstall: + rm ${DESTDIR}${bindir}/linuxband + rm -rf ${DESTDIR}${pkglibdir} + rm -rf ${DESTDIR}${pkgdatadir} + +dist: + @mkdir -p ${DIST_DIR}/${PACKAGE_FULL_NAME} + @for F in ${DIST_FILES}; do \ + T="${DIST_DIR}/${PACKAGE_FULL_NAME}/$$F"; mkdir -p $$(dirname $$T); cp -r $$F $$T; done + find ${DIST_DIR}/${PACKAGE_FULL_NAME} -name '*.pyc' -delete + cd ${DIST_DIR} && tar cfz ${PACKAGE_FULL_NAME}.tar.gz ${PACKAGE_FULL_NAME} + rm -rf ${DIST_DIR}/${PACKAGE_FULL_NAME} + +dist-check: dist + @mkdir -p ${DIST_CHECK_DIR} + tar xfz ${DIST_DIR}/${PACKAGE_FULL_NAME}.tar.gz -C ${DIST_CHECK_DIR} + cd ${DIST_CHECK_DIR}/${PACKAGE_FULL_NAME} && \ + ./configure --prefix=${PWD}/${DIST_CHECK_DIR}/${PACKAGE_FULL_NAME}-install && \ + make && \ + make install + +clean: + rm -rf ${CLEAN_FILES} + +dist-clean: + rm -rf ${DISTCLEAN_FILES} + +maintainer-clean: + rm -rf ${MAINTAINERCLEAN_FILES} + +Makefile: Makefile.in config.status + ./config.status $@ + +config.status: configure + ./config.status --recheck + +.PHONY: \ +all \ +check \ +install \ +uninstall \ +dist \ +dist-check \ +clean \ +dist-clean \ +maintainer-clean + diff --git a/README b/README deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ac2943 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +## LinuxBand + +LinuxBand: A GUI front-end for MMA +WWW: +Email: + +Your feedback on LinuxBand is welcome. Please, see for how to submit suggestions for new features, bug reports and patches. + +### Licensing + +LinuxBand is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +### Documentation + +For installation and usage documentation please see: + +### Release Notes 12.02 Beta + +This is the first release. + +#### Known Issues + +* The linuxband-player is not yet stable. When playing 60sRock, 8Beat, JazzGuitar and a couple of other grooves the incorrect playback position can be displayed. The LinuxBand can even get disconnected from the JACK audio server. When this happens stop the playback and press the JACK reconnect button. Then select some other groove for your song. diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..b9e7c30 --- /dev/null +++ b/autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh +autoreconf --install +automake --add-missing --copy >/dev/null 2>&1 diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..b832d20 --- /dev/null +++ b/configure.ac @@ -0,0 +1,75 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ(2.65) +AC_INIT([linuxband], [12.02], [ales.nosek@gmail.com], [linuxband], [http://linuxband.org]) +AC_CONFIG_SRCDIR([src/main/c/linuxband-player.c]) +AC_CONFIG_HEADER([config.h]) + +# Checks for programs. +AC_PROG_CC +AC_PROG_INSTALL + +# Checks for header files. +AC_HEADER_STDC +AC_CHECK_HEADERS([stdlib.h string.h sys/time.h unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_TYPE_INT8_T +AC_HEADER_TIME +AC_TYPE_UINT16_T +AC_TYPE_UINT32_T +AC_C_VOLATILE + +# Checks for library functions. +AC_FUNC_MALLOC +AC_FUNC_MEMCMP +AC_FUNC_REALLOC +AC_TYPE_SIGNAL +AC_FUNC_STRTOD +AC_CHECK_FUNCS([gettimeofday memset pow strdup strerror strtol]) + +PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.2) +AC_SUBST(GLIB_CFLAGS) +AC_SUBST(GLIB_LIBS) + +PKG_CHECK_MODULES(GTHREAD, gthread-2.0 >= 2.2) +AC_SUBST(GTHREAD_CFLAGS) +AC_SUBST(GTHREAD_LIBS) + +PKG_CHECK_MODULES(JACK, jack >= 0.102.0) +AC_SUBST(JACK_CFLAGS) +AC_SUBST(JACK_LIBS) + +PKG_CHECK_MODULES(SMF, smf >= 1.3) +AC_SUBST(SMF_CFLAGS) +AC_SUBST(SMF_LIBS) + +PKG_CHECK_MODULES(JACK_MIDI_NEEDS_NFRAMES, jack < 0.105.00, + AC_DEFINE(JACK_MIDI_NEEDS_NFRAMES, 1, [whether or not JACK routines need nframes parameter]), true) + +#AC_ARG_WITH([lash], +# [AS_HELP_STRING([--with-lash], +# [support LASH @<:@default=check@:>@])], +# [], +# [with_lash=check]) + +#AS_IF([test "x$with_lash" != xno], +# [PKG_CHECK_MODULES(LASH, lash-1.0, AC_DEFINE([HAVE_LASH], [], [Defined if we have LASH support.]), +# [if test "x$with_lash" != xcheck; then +# AC_MSG_FAILURE([--with-lash was given, but LASH was not found]) +# fi +# ])]) + +#AC_SUBST(LASH_CFLAGS) +#AC_SUBST(LASH_LIBS) + +# Additional install locations +pkgdatadir=${datadir}/${PACKAGE_NAME} +pkglibdir=${libdir}/${PACKAGE_NAME} +AC_SUBST(pkgdatadir) +AC_SUBST(pkglibdir) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/src/main/c/linuxband-player.c b/src/main/c/linuxband-player.c new file mode 100644 index 0000000..4002768 --- /dev/null +++ b/src/main/c/linuxband-player.c @@ -0,0 +1,888 @@ +/* linuxband-player is a proof of concept. It demands to be rewritten. */ +/* + * Copyright (c) 2012 Ales Nosek + * + * This file is part of LinuxBand. + * + * LinuxBand is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* + * linuxband-player is based on code found in jack-smf-player.c, + * written by Edward Tomasz Napierala + * + * Copyright (c) 2007, 2008 Edward Tomasz Napierala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF SCIENCE AND WIN, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "config.h" +#include "smf.h" + +#ifdef WITH_LASH +#include +#endif + +#include "remote_control.h" + +#define PROGRAM_NAME PACKAGE_NAME "-player" +#define PROGRAM_VERSION PACKAGE_VERSION + +#define MIDI_CONTROLLER 0xB0 +#define MIDI_ALL_SOUND_OFF 120 + +#define MAX_NUMBER_OF_TRACKS 128 + +jack_port_t *output_ports[MAX_NUMBER_OF_TRACKS]; +int debug = 0; +jack_client_t *jack_client = NULL; +double rate_limit = 0; +int just_one_output = 0; +int start_stopped = 0; +volatile int use_transport = 1; +int be_quiet = 0; +volatile int playback_started = -1, song_position = 0, ctrl_c_pressed = 0; +volatile int ready_to_roll = FALSE; +volatile int playback_paused = 0; +smf_t * volatile smf_vol = NULL; +static smf_t *smf = NULL; +int remote_control = 0; + +#ifdef WITH_LASH +lash_client_t *lash_client; +#endif + +/* Will emit a warning if time between jack callbacks is longer than this. */ +#define MAX_TIME_BETWEEN_CALLBACKS 0.1 + +/* Will emit a warning if execution of jack callback takes longer than this. */ +#define MAX_PROCESSING_TIME 0.01 + +double +get_time(void) +{ + double seconds; + int ret; + struct timeval tv; + + ret = gettimeofday(&tv, NULL); + + if (ret) { + perror("gettimeofday"); + exit(EX_OSERR); + } + + seconds = tv.tv_sec + tv.tv_usec / 1000000.0; + + return seconds; +} + +double +get_delta_time(void) +{ + static double previously = -1.0; + double now; + double delta; + + now = get_time(); + + if (previously == -1.0) { + previously = now; + + return 0; + } + + delta = now - previously; + previously = now; + + assert(delta >= 0.0); + + return delta; +} + +static gboolean +warning_async(gpointer s) +{ + const char *str = (const char *)s; + + g_warning("%s", str); + + return FALSE; +} + +static void +warn_from_jack_thread_context(const char *str) +{ + g_idle_add(warning_async, (gpointer)str); +} + +static double +nframes_to_ms(jack_nframes_t nframes) +{ + jack_nframes_t sr; + + sr = jack_get_sample_rate(jack_client); + + assert(sr > 0); + + return (nframes * 1000.0) / (double)sr; +} + +// static double +double +nframes_to_seconds(jack_nframes_t nframes) +{ + return nframes_to_ms(nframes) / 1000.0; +} + +static jack_nframes_t +ms_to_nframes(double ms) +{ + jack_nframes_t sr; + + sr = jack_get_sample_rate(jack_client); + + assert(sr > 0); + + return ((double)sr * ms) / 1000.0; +} + +// static jack_nframes_t +jack_nframes_t +seconds_to_nframes(double seconds) +{ + return ms_to_nframes(seconds * 1000.0); +} + +static void +send_all_sound_off(void *port_buffers[MAX_NUMBER_OF_TRACKS], jack_nframes_t nframes) +{ + int i, channel; + unsigned char *buffer; + + for (i = 0; i <= smf->number_of_tracks; i++) { + for (channel = 0; channel < 16; channel++) { +#ifdef JACK_MIDI_NEEDS_NFRAMES + buffer = jack_midi_event_reserve(port_buffers[i], 0, 3, nframes); +#else + buffer = jack_midi_event_reserve(port_buffers[i], 0, 3); +#endif + if (buffer == NULL) { + warn_from_jack_thread_context("jack_midi_event_reserve failed, cannot send All Sound Off."); + break; + } + + buffer[0] = MIDI_CONTROLLER | channel; + buffer[1] = MIDI_ALL_SOUND_OFF; + buffer[2] = 0; + } + + if (just_one_output) + break; + } +} + +static void +process_midi_output(jack_nframes_t nframes) +{ + int i, t, bytes_remaining, track_number; + unsigned char *buffer, tmp_status; + void *port_buffers[MAX_NUMBER_OF_TRACKS]; + jack_nframes_t last_frame_time; + jack_transport_state_t transport_state; + static jack_transport_state_t previous_transport_state = JackTransportStopped; + static int previous_playback_started = -1; + static int previous_playback_paused = 0; + + for (i = 0; i <= smf->number_of_tracks; i++) { + port_buffers[i] = jack_port_get_buffer(output_ports[i], nframes); + + if (port_buffers[i] == NULL) { + warn_from_jack_thread_context("jack_port_get_buffer failed, cannot send anything."); + return; + } + +#ifdef JACK_MIDI_NEEDS_NFRAMES + jack_midi_clear_buffer(port_buffers[i], nframes); +#else + jack_midi_clear_buffer(port_buffers[i]); +#endif + + if (just_one_output) + break; + } + + if (ctrl_c_pressed) { + send_all_sound_off(port_buffers, nframes); + + /* The idea here is to exit at the second time process_midi_output gets called. + Otherwise, All Sound Off won't be delivered. */ + ctrl_c_pressed++; + if (ctrl_c_pressed >= 3) + exit(0); + + return; + } + + // g_debug("PROCESS CALLBACK!!!"); + + if (playback_paused) { + if (!previous_playback_paused) { + send_all_sound_off(port_buffers, nframes); + } + previous_playback_paused = playback_paused; + return; + } + previous_playback_paused = playback_paused; + + if (use_transport) { + + // if (!ready_to_roll) return; + + transport_state = jack_transport_query(jack_client, NULL); + + if (transport_state == JackTransportStopped) { + if (previous_transport_state == JackTransportRolling) { + send_all_sound_off(port_buffers, nframes); + } + playback_started = -1; + } + + if (transport_state == JackTransportStarting) { + playback_started = -1; + } + + if (transport_state == JackTransportRolling) { + if (previous_transport_state != JackTransportRolling) { + jack_position_t position; + jack_transport_query(jack_client, &position); + song_position = position.frame; + playback_started = jack_last_frame_time(jack_client); + } + } + + previous_transport_state = transport_state; + } + else { + if (playback_started == -1) { + if (previous_playback_started >= 0) { + send_all_sound_off(port_buffers, nframes); + send_sond_end(); + } + } + previous_playback_started = playback_started; + } + + + /* End of song already? */ + if (playback_started < 0) + return; + + last_frame_time = jack_last_frame_time(jack_client); + + /* We may push at most one byte per 0.32ms to stay below 31.25 Kbaud limit. */ + bytes_remaining = nframes_to_ms(nframes) * rate_limit; + + double loop_offset = loop_song(smf); + + for (;;) { + + smf_event_t *event = smf_peek_next_event(smf); + + int end_of_song = 0; + int is_meta_event = 0; + + if (event == NULL) { + end_of_song = 1; + } + else if (smf_event_is_metadata(event)) { + is_meta_event = 1; + char *decoded = smf_event_decode(event); + if (decoded) { + if (!be_quiet) + g_debug("Metadata: %s", decoded); + end_of_song = process_meta_event(decoded); + free(decoded); + } + } + + if (end_of_song) { + if (!be_quiet) + g_debug("End of song."); + playback_started = -1; + + if (!use_transport) + ctrl_c_pressed = 1; + break; + } + + if (is_meta_event) { + smf_event_t *ret = smf_get_next_event(smf); + assert(ret != 0); + continue; + } + + bytes_remaining -= event->midi_buffer_length; + + if (rate_limit > 0.0 && bytes_remaining <= 0) { + warn_from_jack_thread_context("Rate limiting in effect."); + break; + } + + // t = seconds_to_nframes(event->time_seconds) + playback_started - song_position + nframes - last_frame_time; + t = seconds_to_nframes(event->time_seconds + loop_offset) + playback_started - song_position - last_frame_time; + + /* If computed time is too much into the future, we'll need + to send it later. */ + if (t >= (int)nframes) + break; + + /* If computed time is < 0, we missed a cycle because of xrun. */ + if (t < 0) + t = 0; + + assert(event->track->track_number >= 0 && event->track->track_number <= MAX_NUMBER_OF_TRACKS); + + /* We will send this event; remove it from the queue. */ + smf_event_t *ret = smf_get_next_event(smf); + assert(ret != 0); + + /* First, send it via midi_out. */ + track_number = 0; + +#ifdef JACK_MIDI_NEEDS_NFRAMES + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length, nframes); +#else + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length); +#endif + + if (buffer == NULL) { + warn_from_jack_thread_context("jack_midi_event_reserve failed, NOTE LOST."); + break; + } + + memcpy(buffer, event->midi_buffer, event->midi_buffer_length); + + /* Ignore per-track outputs? */ + if (just_one_output) + continue; + + /* Send it via proper output port. */ + track_number = event->track->track_number; + +#ifdef JACK_MIDI_NEEDS_NFRAMES + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length, nframes); +#else + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length); +#endif + + if (buffer == NULL) { + warn_from_jack_thread_context("jack_midi_event_reserve failed, NOTE LOST."); + break; + } + + /* Before sending, reset channel to 0. XXX: Not very pretty. */ + assert(event->midi_buffer_length >= 1); + + tmp_status = event->midi_buffer[0]; + + if (event->midi_buffer[0] >= 0x80 && event->midi_buffer[0] <= 0xEF) + event->midi_buffer[0] &= 0xF0; + + memcpy(buffer, event->midi_buffer, event->midi_buffer_length); + + event->midi_buffer[0] = tmp_status; + } +} + +static int +process_callback(jack_nframes_t nframes, void *notused) +{ +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_TIME_BETWEEN_CALLBACKS) { + warn_from_jack_thread_context("Had to wait too long for JACK callback; scheduling problem?"); + } +#endif + + /* Check for impossible condition that actually happened to me, caused by some problem between jackd and OSS4. */ + if (nframes <= 0) { + warn_from_jack_thread_context("Process callback called with nframes = 0; bug in JACK?"); + return 0; + } + + + if (pthread_mutex_trylock(&mutex) == 0) { + if ((smf = smf_vol) != NULL) { + process_midi_output(nframes); + } + pthread_mutex_unlock(&mutex); + } + +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_PROCESSING_TIME) { + warn_from_jack_thread_context("Processing took too long; scheduling problem?"); + } +#endif + + return 0; +} + + +static int +sync_callback(jack_transport_state_t state, jack_position_t *position, void *notused) +{ + assert(jack_client); + g_debug("SYNC_CALLBACK"); + + if (!use_transport) return TRUE; + + int res = FALSE; + if (pthread_mutex_trylock(&mutex) == 0) { + if ((smf = smf_vol) != NULL) { + if (!be_quiet) + g_debug("Seeking to %f seconds.", nframes_to_seconds(position->frame)); + res = loop_seek(smf, nframes_to_seconds(position->frame)); + } + else { + res = TRUE; + } + pthread_mutex_unlock(&mutex); + } + + ready_to_roll = res; + return res; +} + + +void timebase_callback(jack_transport_state_t state, jack_nframes_t nframes, jack_position_t *pos, int new_pos, void *notused) +{ + double min; /* Minutes since frame 0. */ + long abs_tick; /* Ticks since frame 0. */ + long abs_beat; /* Beats since frame 0. */ + smf_tempo_t *tempo; + static smf_tempo_t *previous_tempo = NULL; + + smf_event_t *event = smf_peek_next_event(smf); + if (event == NULL) + return; + + tempo = smf_get_tempo_by_pulses(smf, event->time_pulses); + + assert(tempo); + + if (new_pos || previous_tempo != tempo) { + pos->valid = JackPositionBBT; + pos->beats_per_bar = tempo->numerator; + pos->beat_type = 1.0 / (double)tempo->denominator; + pos->ticks_per_beat = event->track->smf->ppqn; /* XXX: Is this right? */ + pos->beats_per_minute = 60000000.0 / (double)tempo->microseconds_per_quarter_note; + + min = pos->frame / ((double) pos->frame_rate * 60.0); + abs_tick = min * pos->beats_per_minute * pos->ticks_per_beat; + abs_beat = abs_tick / pos->ticks_per_beat; + + pos->bar = abs_beat / pos->beats_per_bar; + pos->beat = abs_beat - (pos->bar * pos->beats_per_bar) + 1; + pos->tick = abs_tick - (abs_beat * pos->ticks_per_beat); + pos->bar_start_tick = pos->bar * pos->beats_per_bar * pos->ticks_per_beat; + pos->bar++; /* adjust start to bar 1 */ + + previous_tempo = tempo; + + } else { + /* Compute BBT info based on previous period. */ + pos->tick += nframes * pos->ticks_per_beat * pos->beats_per_minute / (pos->frame_rate * 60); + + while (pos->tick >= pos->ticks_per_beat) { + pos->tick -= pos->ticks_per_beat; + if (++pos->beat > pos->beats_per_bar) { + pos->beat = 1; + ++pos->bar; + pos->bar_start_tick += pos->beats_per_bar * pos->ticks_per_beat; + } + } + } +} + +/* Connects to the specified input port, disconnecting already connected ports. */ +int +connect_to_input_port(const char *port) +{ + int ret; + + ret = jack_port_disconnect(jack_client, output_ports[0]); + + if (ret) { + g_warning("Cannot disconnect MIDI port."); + + return -3; + } + + ret = jack_connect(jack_client, jack_port_name(output_ports[0]), port); + + if (ret) { + g_warning("Cannot connect to %s.", port); + + return -4; + } + + g_warning("Connected to %s.", port); + + return 0; +} + +static void +init_jack(void) +{ + int i, err; + +#ifdef WITH_LASH + lash_event_t *event; +#endif + + jack_client = jack_client_open(PROGRAM_NAME, JackNoStartServer, NULL); + + if (jack_client == NULL) { + g_critical("Could not connect to the JACK server; run jackd first?"); + exit(EX_UNAVAILABLE); + } + +#ifdef WITH_LASH + event = lash_event_new_with_type(LASH_Client_Name); + assert (event); /* Documentation does not say anything about return value. */ + lash_event_set_string(event, jack_get_client_name(jack_client)); + lash_send_event(lash_client, event); + + lash_jack_client_name(lash_client, jack_get_client_name(jack_client)); +#endif + + err = jack_set_process_callback(jack_client, process_callback, 0); + if (err) { + g_critical("Could not register JACK process callback."); + exit(EX_UNAVAILABLE); + } + + if (use_transport) { + err = jack_set_sync_callback(jack_client, sync_callback, 0); + if (err) { + g_critical("Could not register JACK sync callback."); + exit(EX_UNAVAILABLE); + } +#if 0 + err = jack_set_timebase_callback(jack_client, 1, timebase_callback, 0); + if (err) { + g_critical("Could not register JACK timebase callback."); + exit(EX_UNAVAILABLE); + } +#endif + } + + jack_on_shutdown(jack_client, jack_shutdown, 0); + + int number_of_tracks; + if (remote_control) { + number_of_tracks = 0; // TODO allow more ports + } + else { + assert(smf->number_of_tracks >= 1); + number_of_tracks = smf->number_of_tracks; + } + + /* We are allocating number_of_tracks + 1 output ports. */ + for (i = 0; i <= number_of_tracks; i++) { + char port_name[32]; + + if (i == 0) + snprintf(port_name, sizeof(port_name), "midi_out"); + else + snprintf(port_name, sizeof(port_name), "track_%d_midi_out", i); + + output_ports[i] = jack_port_register(jack_client, port_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); + + if (output_ports[i] == NULL) { + g_critical("Could not register JACK output port '%s'.", port_name); + exit(EX_UNAVAILABLE); + } + + if (just_one_output) + break; + } + + if (jack_activate(jack_client)) { + g_critical("Cannot activate JACK client."); + exit(EX_UNAVAILABLE); + } +} + +#ifdef WITH_LASH + +static gboolean +lash_callback(gpointer notused) +{ + lash_event_t *event; + + while ((event = lash_get_event(lash_client))) { + switch (lash_event_get_type(event)) { + case LASH_Restore_Data_Set: + case LASH_Save_Data_Set: + break; + + case LASH_Quit: + g_warning("Exiting due to LASH request."); + ctrl_c_pressed = 1; + break; + + default: + g_warning("Receieved unknown LASH event of type %d.", lash_event_get_type(event)); + lash_event_destroy(event); + } + } + + return TRUE; +} + +static void +init_lash(lash_args_t *args) +{ + /* XXX: Am I doing the right thing wrt protocol version? */ + lash_client = lash_init(args, PROGRAM_NAME, LASH_Config_Data_Set, LASH_PROTOCOL(2, 0)); + + if (!lash_server_connected(lash_client)) { + g_critical("Cannot initialize LASH. Continuing anyway."); + /* exit(EX_UNAVAILABLE); */ + + return; + } + + /* Schedule a function to process LASH events, ten times per second. */ + g_timeout_add(100, lash_callback, NULL); +} + +#endif /* WITH_LASH */ + +/* + * This is neccessary for exiting due to jackd being killed, when exit(0) + * in process_callback won't get called for obvious reasons. + */ +gboolean +emergency_exit_timeout(gpointer notused) +{ + if (ctrl_c_pressed == 0) + return TRUE; + + exit(0); +} + +void +ctrl_c_handler(int signum) +{ + ctrl_c_pressed = 1; +} + +static void +log_handler(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer notused) +{ + if (log_level >= G_LOG_LEVEL_DEBUG && !debug) { + return; + } + if (!log_domain) { + log_domain = PROGRAM_NAME; + } + fprintf(stderr, "%s: %s\n", log_domain, message); +} + +static void +show_version(void) +{ + fprintf(stdout, "%s %s, libsmf %s\n", PROGRAM_NAME, PROGRAM_VERSION, smf_get_version()); + + exit(EX_OK); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: " PROGRAM_NAME " [-dnqstV] [ -a ] [-r ] file_name\n"); + + exit(EX_USAGE); +} + +int +main(int argc, char *argv[]) +{ + int ch; + char *file_name, *autoconnect_port_name = NULL; + + +#ifdef WITH_LASH + lash_args_t *lash_args; +#endif + + g_thread_init(NULL); + +#ifdef WITH_LASH + lash_args = lash_extract_args(&argc, &argv); +#endif + + g_log_set_default_handler(log_handler, NULL); + + while ((ch = getopt(argc, argv, "a:dnqr:stVx")) != -1) { + switch (ch) { + case 'a': + autoconnect_port_name = strdup(optarg); + break; + + case 'd': + debug = 1; + break; + + case 'n': + start_stopped = 1; + break; + + case 'q': + be_quiet = 1; + break; + + case 'r': + rate_limit = strtod(optarg, NULL); + if (rate_limit <= 0.0) { + g_critical("Invalid rate limit specified.\n"); + + exit(EX_USAGE); + } + + break; + + case 's': + just_one_output = 1; + break; + + case 't': + use_transport = 0; + break; + + case 'x': + remote_control = 1; + break; + + case 'V': + show_version(); + break; + + case '?': + default: + usage(); + break; + } + } + + argc -= optind; + argv += optind; + + if (argv[0] == NULL) { + g_critical("No file name given."); + usage(); + } + + file_name = argv[0]; + + if (!remote_control) { + smf_vol = smf = smf_load(file_name); + + if (smf == NULL) { + g_critical("Loading SMF file failed."); + + exit(-1); + } + + if (!be_quiet) + g_message("%s.", smf_decode(smf)); + + if (smf->number_of_tracks > MAX_NUMBER_OF_TRACKS) { + g_warning("Number of tracks (%d) exceeds maximum for per-track output; implying '-s' option.", smf->number_of_tracks); + just_one_output = 1; + } + } + +#ifdef WITH_LASH + init_lash(lash_args); +#endif + + g_timeout_add(1000, emergency_exit_timeout, (gpointer)0); + signal(SIGINT, ctrl_c_handler); + + init_jack(); + + if (autoconnect_port_name) { + if (connect_to_input_port(autoconnect_port_name)) { + g_critical("Couldn't connect to '%s', exiting.", autoconnect_port_name); + exit(EX_UNAVAILABLE); + } + } + + if (use_transport && !start_stopped) { + jack_transport_locate(jack_client, 0); + jack_transport_start(jack_client); + } + + if (!use_transport) + playback_started = jack_frame_time(jack_client); + + if (remote_control) + remote_control_start(argv[0]); + + g_main_loop_run(g_main_loop_new(NULL, TRUE)); + + /* Not reached. */ + + return 0; +} + diff --git a/src/main/c/midi.c b/src/main/c/midi.c new file mode 100644 index 0000000..2e47bfd --- /dev/null +++ b/src/main/c/midi.c @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2012 Ales Nosek + * + * This file is part of LinuxBand. + * + * LinuxBand is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "smf.h" +#include "midi.h" + +int +strcmp_min(char *s1, char *s2) { + int len = strlen(s1) < strlen(s2) ? strlen(s1) : strlen(s2); + return strncmp(s1, s2, len); +} + +char * skip_marker_tag(char *event) { + if (strcmp_min(event, MARKER_TAG) == 0) + return event + strlen(MARKER_TAG); + else + return NULL; +} + +char * +decode_barnum(char *event) { + char *s = event; + if ((s = skip_marker_tag(s)) != NULL) { + if (strcmp_min(s, BAR_TAG) == 0) { + return s + strlen(BAR_TAG); + } + } + return NULL; +} + +char * +decode_linenum(char *event) { + char *s = event; + if ((s = skip_marker_tag(s)) != NULL) { + if (isdigit(*s)) { + return s; + } + } + return NULL; +} + +char * +decode_end(char *event) { + char *s = event; + if ((s = skip_marker_tag(s)) != NULL) { + if (strcmp_min(s, END_TAG) == 0) { + return s; + } + } + return NULL; +} + +void +show_track(smf_track_t *track) { + g_debug("TRACK %i\n", track->track_number); + int i = 1; + smf_event_t *event; + while ((event = smf_track_get_event_by_number(track, i)) != NULL) { + g_debug("%6i %s\n", event->time_pulses, smf_event_decode(event)); + i++; + } +} + +void +show_smf(smf_t *smf) { + g_debug("%s\n", smf_decode(smf)); + int i = 1; + smf_track_t *track; + while ((track = smf_get_track_by_number(smf, i)) != NULL) { + show_track(track); + i++; + } +} + +smf_event_t * +find_song_end(smf_t *smf) { + smf_event_t *event = NULL, *end = NULL; + while ((event = smf_get_next_event(smf)) != NULL) { + if (smf_event_is_metadata(event)) { + char *decoded = smf_event_decode(event); + if (decode_end(decoded) != NULL) { + end = event; + } + free(decoded); + } + if (end != NULL) break; + } + assert(end); + return(end); +} + +smf_event_t * +find_bar_end(smf_t *smf) { + smf_event_t *event = NULL, *end = NULL; + while ((event = smf_get_next_event(smf)) != NULL) { + if (smf_event_is_metadata(event)) { + char *decoded = smf_event_decode(event); + if ((decode_barnum(decoded)) != NULL || (decode_end(decoded)) != NULL) { + end = event; + } + free(decoded); + } + if (end != NULL) break; + } + assert(end); + return(end); +} + +smf_event_t * +find_bar_num(smf_t *smf, int barnum) { + smf_event_t *event = NULL, *start = NULL; + while ((event = smf_get_next_event(smf)) != NULL) { + if (smf_event_is_metadata(event)) { + char *decoded = smf_event_decode(event); + char *s; + if ((s = decode_barnum(decoded)) != NULL) { + if (atoi(s) == barnum){ + start = event; + } + } + free(decoded); + } + if (start != NULL) break; + } + assert(start); + return(start); +} + +double +find_bar_num_seconds(smf_t *smf, int barnum) { + smf_rewind(smf); + smf_event_t *event = find_bar_num(smf, barnum); + return event->time_seconds; +} + +void +get_bar_offsets(smf_t *smf, int barnum, int result[2]) { + smf_rewind(smf); + smf_event_t *start = find_bar_num(smf, barnum); + smf_event_t *end = find_bar_end(smf); + result[0] = start->time_pulses; + result[1] = end->time_pulses; +} + +smf_event_t * +copy_event(smf_event_t *event) { + char *buff = event->midi_buffer; + int len = event->midi_buffer_length; + return smf_event_new_from_pointer(buff, len); +} + +void +copy_events(smf_t *smf, smf_t *smf2, int start, int end, int start2) { + int ret = smf_seek_to_pulses(smf, start); + assert(ret == 0); + smf_event_t *event; + while ((event = smf_get_next_event(smf)) != NULL && event->time_pulses < end) { + int time = event->time_pulses; + int track_num = event->track_number; + smf_track_t *track = smf_get_track_by_number(smf2, track_num); + smf_track_add_event_pulses(track, copy_event(event), start2 + time - start); + } +} + +smf_event_t * +create_marker(char *text) { + char meta_type = 0xFF; + char marker_type = 0x06; + int text_len = strlen(text); + smf_event_t *event = smf_event_new(); + event->midi_buffer_length = 3 + text_len; + event->midi_buffer = malloc(event->midi_buffer_length); + event->midi_buffer[0] = meta_type; + event->midi_buffer[1] = marker_type; + event->midi_buffer[2] = text_len; + memcpy(event->midi_buffer + 3, text, text_len); + return event; +} + +smf_t * +create_empty_smf(smf_t *smf) { + smf_t *smf2 = smf_new(); + int ret = smf_set_ppqn(smf2, smf->ppqn); + assert(ret == 0); + ret = smf_set_format(smf2, smf->format); + assert(ret == 0); + + // create tracks + int i; + for (i = 0; i < smf->number_of_tracks; i++) { + smf_track_t *track = smf_track_new(); + smf_add_track(smf2, track); + } + return smf2; +} + +smf_t * +copy_smf(smf_t *smf) { + smf_t *smf2 = create_empty_smf(smf); + copy_events(smf, smf2, 0, INT_MAX, 0); + return smf2; +} + +smf_t * +copy_smf_bars(smf_t *smf, int bar_start, int bar_end) { + + smf_t *smf2 = create_empty_smf(smf); + + // copy time 0 meta events + smf_rewind(smf); + smf_track_t *meta_track = smf_get_track_by_number(smf, 1); + smf_track_t *meta_track2 = smf_get_track_by_number(smf2, 1); + assert(meta_track); + assert(meta_track2); + smf_event_t * event; + while ((event = smf_track_get_next_event(meta_track)) != NULL && event->time_pulses == 0) { + if (decode_barnum(smf_event_decode(event)) == NULL) { + smf_event_t * new_event = copy_event(event); + smf_track_add_event_pulses(meta_track2, new_event, 0); + } + } + + // copy bars + int i; + int start2 = 0; + for (i = bar_start; i <= bar_end; i++) { + int offsets[2]; + get_bar_offsets(smf, i, offsets); + int start = offsets[0], end = offsets[1]; + copy_events(smf, smf2, start, end, start2); + start2 += end - start; + } + + // end of the song event + smf_event_t *end = create_marker(END_TAG); + smf_track_add_event_pulses(meta_track2, end, start2); + return smf2; +} + + +void +loop_smf(smf_t *smf, int intro_length, int tags[], double tags_secs[]) { + smf_rewind(smf); + int i; + // find end of introduction + for (i = 0; i <= intro_length; i++) { + smf_event_t *intro_end = find_bar_end(smf); + tags[0] = intro_end->time_pulses; + tags_secs[0] = intro_end->time_seconds; + } + // find end of song + smf_rewind(smf); + smf_event_t *song_end = find_song_end(smf); + tags[1] = song_end->time_pulses; + tags_secs[1] = song_end->time_seconds; + // compute song length + tags[2] = tags[1] - tags[0]; + tags_secs[2] = tags_secs[1] - tags_secs[0]; + // copy it after the end of song + copy_events(smf, smf, tags[0], tags[1], tags[1]); + // end of the song event + smf_event_t *end_event = create_marker(END_TAG); + smf_track_t *meta_track = smf_get_track_by_number(smf, 1); + assert(meta_track); + smf_track_add_event_pulses(meta_track, end_event, tags[1] + tags[2]); +} + +smf_t * +load_smf_data(char *buffer, int length) { + smf_t *smf = smf_load_from_memory(buffer, length); + if (smf == NULL) { + g_error("Loading of SMF file failed."); + } + g_debug("%s.", smf_decode(smf)); + return smf; +} diff --git a/src/main/c/midi.h b/src/main/c/midi.h new file mode 100644 index 0000000..ed1f6b9 --- /dev/null +++ b/src/main/c/midi.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012 Ales Nosek + * + * This file is part of LinuxBand. + * + * LinuxBand is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef JACKSMFPLAYERMIDI_H_ +#define JACKSMFPLAYERMIDI_H_ + +#define MARKER_TAG "Marker: " +#define BAR_TAG "BAR" +#define END_TAG "END" + +char * +decode_barnum(char *event); + +char * +decode_linenum(char *event); + +void +show_smf(smf_t *smf); + +double +find_bar_num_seconds(smf_t *smf, int barnum); + +void +get_bar_offsets(smf_t *smf, int barnum, int result[2]); + +smf_t * +copy_smf(smf_t *smf); + +smf_t * +copy_smf_bars(smf_t *smf, int bar_start, int bar_end); + +void +loop_smf(smf_t *smf, int intro_length, int tags[], double tags_secs[]); + +smf_t * +load_smf_data(char *buffer, int length); + +char * +decode_end(char *event); + + +#endif /* JACKSMFPLAYERMIDI_H_ */ diff --git a/src/main/c/remote_control.c b/src/main/c/remote_control.c new file mode 100644 index 0000000..160b718 --- /dev/null +++ b/src/main/c/remote_control.c @@ -0,0 +1,516 @@ +/* + * Copyright (c) 2012 Ales Nosek + * + * This file is part of LinuxBand. + * + * LinuxBand is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "smf.h" +#include "remote_control.h" +#include "midi.h" + +FILE *input_pipe = NULL; +FILE *output_pipe = NULL; + +#define MAX_TOKEN_LENGTH 30 +#define TOKEN_SEPARATOR ' ' + +typedef enum { + LOAD, + PLAY, + PLAY_BAR, + PLAY_BARS, + STOP, + PAUSE_ON, + PAUSE_OFF, + LOOP_ON, + LOOP_OFF, + TRANSPORT_ON, + TRANSPORT_OFF, + INTRO_LENGTH, + FINISH, + UNKNOWN +} COMMAND; + +#define COMMAND_MAP_SIZE UNKNOWN +char *command_map[COMMAND_MAP_SIZE]; + +void init_command_map() { + command_map[LOAD] = "LOAD"; + command_map[PLAY] = "PLAY"; + command_map[PLAY_BAR] = "PLAY_BAR"; + command_map[PLAY_BARS] = "PLAY_BARS"; + command_map[STOP] = "STOP"; + command_map[PAUSE_ON] = "PAUSE_ON"; + command_map[PAUSE_OFF] = "PAUSE_OFF"; + command_map[LOOP_ON] = "LOOP_ON"; + command_map[LOOP_OFF] = "LOOP_OFF"; + command_map[TRANSPORT_ON] = "TRANSPORT_ON"; + command_map[TRANSPORT_OFF] = "TRANSPORT_OFF"; + command_map[INTRO_LENGTH] = "INTRO_LENGTH"; + command_map[FINISH] = "FINISH"; +} + +#define FEEDBACK_BARNUM "BAR_NUMBER" +#define FEEDBACK_LINENUM "LINE_NUMBER" +#define FEEDBACK_SONGEND "SONG_END" + +#define PARTIAL_SEEK_LENGTH 10 + +extern smf_t * volatile smf_vol; +extern jack_client_t *jack_client; +extern volatile int use_transport; +extern volatile int playback_started; +extern volatile int song_position; +extern volatile int playback_paused; + +static smf_t *orig_smf = NULL; // smf data as received from client +static volatile int loop = 1; // should loop? +static int intro_bar_length = 2; // intro bars count +static int tags[3]; // end of intro, end of the song, length of song (all in pulses) +static double tags_secs[3]; // the same in seconds +static int loop_count = 0; +static double loop_offset = 0; + +extern jack_nframes_t +seconds_to_nframes(double seconds); + +pthread_mutex_t mutex; + +void +open_pipes(char *output_pipe_name) { + input_pipe = fdopen(0, "r"); + if (!input_pipe) { + g_error("Cannot open stdin for reading. %s", strerror(errno)); + } + g_debug("Input pipe: stdin"); + int output_pipe_fd = open(output_pipe_name, O_WRONLY | O_NONBLOCK); + if (output_pipe_fd == -1) { + g_error("Failed to open output pipe '%s'. %s", output_pipe_name, strerror(errno)); + } + else { + output_pipe = fdopen(output_pipe_fd, "w"); + if (!output_pipe){ + g_error("%s", strerror(errno)); + } + } + g_debug("Output pipe: '%s'", output_pipe_name); +} + +void +next_token(FILE *pipe, char* buffer) { + memset(buffer, 0, MAX_TOKEN_LENGTH); + int i = 0; + char c = -1; + while (c != TOKEN_SEPARATOR && i < MAX_TOKEN_LENGTH - 1) { + if (fread(&c, sizeof(c), 1, pipe) != 1) break; + buffer[i] = c; + i++; + } + buffer[--i] = 0; +} + +COMMAND +next_command(FILE *pipe) { + char token[MAX_TOKEN_LENGTH]; + next_token(pipe, token); + int i; + for (i = 0; i < COMMAND_MAP_SIZE; i++) { + if (strcmp(token, command_map[i]) == 0) return i; + } + return UNKNOWN; +} + +double +loop_song(smf_t *smf) { + int intro_end = tags[0]; + int song_end = tags[1]; + int song_length = tags[2]; + smf_event_t *event = smf_peek_next_event(smf); + int next = event->time_pulses; + if (loop) { + if (next > song_end) { + // g_debug("SONG_LENGTH %i", song_length); + // g_debug("NEXT %i", next); + g_debug("LOOP. Seeking to %d pulses.", next - song_length); + int ret = smf_seek_to_pulses(smf, next - song_length); + assert(ret == 0); + smf_event_t *event2 = smf_peek_next_event(smf); + loop_offset = (event->time_seconds - event2->time_seconds) * ++loop_count; + // g_debug("Event1 = %lf, event2 = %lf", event->time_seconds, event2->time_seconds); + } + } + // g_debug("OFFSET %f", loop_offset); + return loop_offset; +} + +int +check_event(smf_event_t *event, double seconds) { + if (event == NULL) { + g_critical("Trying to seek past the end of song."); + return TRUE; + } + else { + return event->time_seconds >= seconds; + } +} + +int +partial_seek(smf_t *smf, double seconds) { + smf_event_t* event = smf_peek_next_event(smf); + // g_debug("Partial seek enter: TARGET %f, CURRENT %f", seconds, event->time_seconds); + if (event == NULL || event->time_seconds >= seconds) { + smf_rewind(smf); + } + + // check first event + event = smf_peek_next_event(smf); + if (check_event(event, seconds)) { + g_debug("TARGET %f, CURRENT %f", seconds, event->time_seconds); + return TRUE; + } + + int i = 0; + while (i < PARTIAL_SEEK_LENGTH) { + smf_event_t *ret = smf_get_next_event(smf); + assert(ret != 0); + event = smf_peek_next_event(smf); + if (check_event(event, seconds)) { + g_debug("TARGET %f, CURRENT %f", seconds, event->time_seconds); + return TRUE; + } + i++; + } + g_debug("TARGET %f, CURRENT %f", seconds, event->time_seconds); + return FALSE; +} + +int +loop_seek(smf_t *smf, double seconds) { + double intro_end = tags_secs[0]; + double song_end = tags_secs[1]; + double song_length = tags_secs[2]; + // g_debug("LOOP_SEEK CALLED"); + // g_debug("INTRO_END=%f, SONG_END=%f, SONG_LENGTH=%f", intro_end, song_end, song_length); + g_debug("SECONDS = %f", seconds); + loop_count = 0; + loop_offset = 0; + if (seconds > song_end) { + while (seconds > intro_end + (loop_count + 1) * song_length) { + loop_count++; + // g_debug("LOOP_COUNT++"); + } + loop_offset = loop_count * song_length; + seconds = seconds - loop_offset; + // g_debug("LOOP_OFFSET %f", loop_offset); + } + // g_debug("LOOP_COUNT %i, LOOP_OFFSET %f", loop_count, loop_offset); + return partial_seek(smf, seconds); +} + +void +free_smf() { + if (smf_vol != NULL && smf_vol != orig_smf) smf_delete(smf_vol); +} + +void +start_playback() { + loop_count = 0; + loop_offset = 0; + g_debug("Use transport %i", use_transport); + if (use_transport) { + jack_transport_start(jack_client); + } + else { + playback_started = jack_frame_time(jack_client); + } + playback_paused = 0; +} + +void +command_load(FILE *pipe) { + char token[MAX_TOKEN_LENGTH]; + next_token(pipe, token); + int count = atoi(token); + g_debug("MIDI data length = %i", count); + char *buff = (char *) malloc(count); + int read = fread(buff, sizeof(char), count, pipe); + if (read < count) { + g_error("Error while reading MIDI data. Marker: Expected %i bytes, read %i.", count, read); + } + g_debug("MIDI data %i bytes read", count); + if (orig_smf != NULL) smf_delete(orig_smf); + orig_smf = load_smf_data(buff, count); + free(buff); +} + +void +command_stop() { + if (use_transport) { + jack_transport_stop(jack_client); + send_sond_end(); + } + else { + playback_started = -1; + } + playback_paused = 0; +} + +void +command_play() { + command_stop(); + pthread_mutex_lock(&mutex); + smf_t *smf = copy_smf(orig_smf); + pthread_mutex_unlock(&mutex); + loop_smf(smf, intro_bar_length, tags, tags_secs); + smf_rewind(smf); + pthread_mutex_lock(&mutex); + free_smf(); + smf_vol = smf; + pthread_mutex_unlock(&mutex); + if (use_transport) { + jack_transport_locate(jack_client, 0); + } + else { + song_position = 0; + } + start_playback(); +} + +void +command_play_from_bar(FILE *pipe) { + char token[MAX_TOKEN_LENGTH]; + next_token(pipe, token); + int start = atoi(token); + g_debug("PLAYING FROM BAR %i", start); + command_stop(); + pthread_mutex_lock(&mutex); + smf_t *smf = copy_smf(orig_smf); + pthread_mutex_unlock(&mutex); + loop_smf(smf, intro_bar_length, tags, tags_secs); + smf_rewind(smf); + double start_time = find_bar_num_seconds(smf, start); + g_debug("START_TIME = %f", start_time); + pthread_mutex_lock(&mutex); + free_smf(); + smf_vol = smf; + if (use_transport) { + jack_transport_locate(jack_client, seconds_to_nframes(start_time)); + } + else { + int ret = smf_seek_to_seconds(smf, start_time); + assert(ret == 0); + song_position = seconds_to_nframes(start_time); + } + pthread_mutex_unlock(&mutex); + start_playback(); +} + +void +command_play_bars(FILE *pipe) { + char token[MAX_TOKEN_LENGTH]; + next_token(pipe, token); + int start = atoi(token); + next_token(pipe, token); + int end = atoi(token); + command_stop(); + pthread_mutex_lock(&mutex); + smf_t *smf = copy_smf_bars(orig_smf, start, end); + pthread_mutex_unlock(&mutex); + loop_smf(smf, 0, tags, tags_secs); + smf_rewind(smf); + pthread_mutex_lock(&mutex); + free_smf(); + smf_vol = smf; + pthread_mutex_unlock(&mutex); + if (use_transport) { + jack_transport_locate(jack_client, 0); + } + else { + song_position = 0; + } + start_playback(); +} + +void +command_pause(int on) { + if (on) { + if (use_transport && !playback_paused) { + jack_transport_stop(jack_client); + } + playback_paused = 1; + } + else { + if (use_transport && playback_paused) { + jack_transport_start(jack_client); + } + playback_paused = 0; + } +} + +void +command_loop(int on) { + loop = on; +} + +void +command_transport(int on) { + use_transport = on; +} + +void +command_intro_length(FILE *pipe) { + char token[MAX_TOKEN_LENGTH]; + next_token(pipe, token); + intro_bar_length = atoi(token); + g_debug("Intro length = %i", intro_bar_length); +} + +void +command_finish() { + jack_client_close(jack_client); + exit(0); +} + +/** + * This is the shutdown callback for this JACK application. + * It is called by JACK if the server ever shuts down or + * decides to disconnect the client. + */ + void + jack_shutdown(void *arg) + { + g_debug("Jack shutdown."); + exit(EX_UNAVAILABLE); + } + +void +remote_control_loop(FILE *input_pipe, FILE *output_pipe) { + int loop = 1; + while (loop) { + COMMAND command = next_command(input_pipe); + switch(command) { + case LOAD: + g_debug("Load"); + command_load(input_pipe); + break; + case PLAY: + g_debug("Play"); + command_play(); + break; + case PLAY_BAR: + g_debug("Play from bar"); + command_play_from_bar(input_pipe); + break; + case PLAY_BARS: + g_debug("Play bars"); + command_play_bars(input_pipe); + break; + case STOP: + g_debug("Stop"); + command_stop(); + break; + case PAUSE_ON: + g_debug("Pause On"); + command_pause(1); + break; + case PAUSE_OFF: + g_debug("Pause Off"); + command_pause(0); + break; + case LOOP_ON: + g_debug("Loop On"); + command_loop(1); + break; + case LOOP_OFF: + g_debug("Loop Off"); + command_loop(0); + break; + case TRANSPORT_ON: + g_debug("Transport On"); + command_transport(1); + break; + case TRANSPORT_OFF: + g_debug("Transport Off"); + command_transport(0); + break; + case INTRO_LENGTH: + g_debug("Intro Length"); + command_intro_length(input_pipe); + break; + case FINISH: + g_debug("Finish"); + command_finish(); + break; + case UNKNOWN: + g_error("Received unknown command. Exiting ..."); + break; + } + } +} + +void +remote_control_start(char *output_pipe_name) { + open_pipes(output_pipe_name); + init_command_map(); + remote_control_loop(input_pipe, output_pipe); +} + +void +send_token(char *token) { + int res = fprintf(output_pipe, "%s ", token); + if (res == -1) + g_error("Failed to send token %s. %s", token, strerror(errno)); + fflush(output_pipe); +} + +int +process_meta_event(char *event) { + // tell the controller to move its play head + char *s; + if ((s = decode_barnum(event)) != NULL) { + send_token(FEEDBACK_BARNUM); + send_token(s); + } + else if ((s = decode_linenum(event)) != NULL) { + send_token(FEEDBACK_LINENUM); + send_token(s); + } + else if (decode_end(event) && !loop) { + send_token(FEEDBACK_SONGEND); + return 1; + } + return 0; +} + +void +send_sond_end() { + send_token(FEEDBACK_SONGEND); +} + diff --git a/src/main/c/remote_control.h b/src/main/c/remote_control.h new file mode 100644 index 0000000..f40262f --- /dev/null +++ b/src/main/c/remote_control.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012 Ales Nosek + * + * This file is part of LinuxBand. + * + * LinuxBand is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef JACKSMFPLAYERRC_H_ +#define JACKSMFPLAYERRC_H_ + +void +remote_control_start(char *pipe_name); + +double +loop_song(smf_t *smf); + +int +loop_seek(smf_t *smf, double pos); + +int +process_meta_event(char *event); + +void +jack_shutdown(void *arg); + +void +send_sond_end(); + +extern pthread_mutex_t mutex; + +#endif /* JACKSMFPLAYERRC_H_ */ diff --git a/src/main/config/default.mma b/src/main/config/default.mma new file mode 100644 index 0000000..1c3e610 --- /dev/null +++ b/src/main/config/default.mma @@ -0,0 +1,11 @@ +Tempo 120 +Swingmode on +Groove Swing + +1 Dm +2 / +3 G7 +4 / +5 CM7 +6 / + diff --git a/src/main/config/linuxband.rc.in b/src/main/config/linuxband.rc.in new file mode 100644 index 0000000..b864c5e --- /dev/null +++ b/src/main/config/linuxband.rc.in @@ -0,0 +1,15 @@ +[Saved] +intro_length = 0 +jack_transport = False +loop = True +work_dir = + +[Preferences] +jack_connect_startup = True +mma_grooves_path = /usr/share/mma/lib/stdlib +chord_sheet_font = Verdana 12 +mma_path = /usr/bin/mma +template_file = @pkgdatadir@/default.mma + +[Version] +program_version = @PACKAGE_VERSION@ \ No newline at end of file diff --git a/src/main/glade/gui.glade b/src/main/glade/gui.glade new file mode 100644 index 0000000..284523b --- /dev/null +++ b/src/main/glade/gui.glade @@ -0,0 +1,2385 @@ + + + + + + 1100 + 700 + GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK + Linux Band + audio-x-generic + + + + + + True + vertical + + + True + + + True + _File + True + + + True + + + gtk-new + True + True + True + + + + + + gtk-open + True + True + True + + + + + + True + Recent songs + True + + + + + True + + + + + gtk-save + True + False + True + True + + + + + + gtk-save-as + True + True + True + + + + + + Export MIDI + True + False + + + + True + gtk-save-as + + + + + + + True + + + + + gtk-quit + True + True + True + + + + + + + + + + True + _Edit + True + + + True + + + gtk-cut + True + True + True + + + + + + gtk-copy + True + True + True + + + + + + gtk-paste + True + True + True + + + + + + gtk-delete + True + True + True + + + + + + True + + + + + gtk-select-all + True + True + True + + + + + + + + + + True + _View + True + + + True + + + True + _Chordsheet + True + True + True + + + + + + True + _MMA Source + True + True + menuitem5 + + + + + True + + + + + gtk-preferences + True + True + True + + + + + + + + + + True + _Help + True + + + True + + + gtk-about + True + True + True + + + + + + + + + + False + 0 + + + + + True + + + True + both + + + True + Create a new song [Ctrl+N] + gtk-new + + + + False + True + + + + + True + Open song file [Ctrl+O] + gtk-open + + + + False + + + + + True + False + Save song file [Ctrl+S] + gtk-save + + + + False + True + + + + + True + Save as + gtk-save-as + + + + False + True + + + + + True + Export song as MIDI file (.MID) + Export MIDI + True + gtk-save-as + + + + False + True + + + + + True + + + False + + + + + True + Play song [Space]. If Ctrl key is held down, will play from current bar. If Shift is held down, will play selected bars. If Ctrl+Shift is held down, only compiles the song and reports errors if any + gtk-media-play + + + False + True + + + + + True + Stop playback [Space] + gtk-media-stop + + + + False + True + + + + + True + Pause playback + gtk-media-pause + + + + False + True + + + + + True + + + False + True + + + + + True + Restart JACK client. Good when Linux Band stops communicating with JACK server + JACK reconnect + gtk-refresh + + + + False + True + + + + + True + + + True + vertical + + + Loop + True + False + False + Loop playback + True + True + + + + 0 + + + + + JACK transport + True + True + False + If enabled, Linuxband will play synchronized with other JACK clients + False + True + True + + + + 1 + + + + + + + False + True + + + + + 0 + + + + + True + vertical + + + True + + + 300 + True + True + Song title. Whatever is found in the comment on the first line of MMA file + + + + + 0 + + + + + 0 + + + + + True + + + togglebutton + True + True + True + Global groove. The first groove event in the MMA file + False + + + + 0 + + + + + togglebutton + True + True + True + Global tempo. The first tempo event in the MMA file + False + + + + 1 + + + + + True + True + Number of song bars. Decrementing deletes last bar. Incrementing appends empty bar + + 1 + 0 0 100 1 10 0 + True + + + + + 2 + + + + + True + True + The length of song intro. When playing in the loop, intro is played only for the first time + + 1 + 2 0 100 1 10 0 + True + + + + + 3 + + + + + 1 + + + + + False + False + 1 + + + + + False + 1 + + + + + True + True + vertical + 550 + True + + + True + vertical + + + True + True + bottom + False + False + 0 + 0 + 0 + + + True + vertical + + + 25 + True + + + True + + + True + + + 0 + + + + + True + True + True + + + + True + + + True + Add event to the song. E.g. tempo change, groove change, repetition ... + 5 + Add Event + + + 0 + + + + + True + + + False + 1 + + + + + 15 + True + down + + + False + False + 2 + + + + + + + 1 + + + + + False + 0 + + + + + True + 5 + + + True + Bar chords + + + 0 + + + + + + + + True + True + cursor + Chord played on the first beat. Use [1], [2], [3], [4] keys to jump to beat chord edit field + 20 + 15 + + + + + 2 + + + + + True + True + Chord played on the second beat. If empty, previous chord will apply + 20 + 15 + + + + + 3 + + + + + True + True + Chord played on the third beat + 20 + 15 + + + + + 4 + + + + + True + True + Chord played on the fourth beat + 20 + 15 + + + + + 5 + + + + + True + True + Chord played on the fifth beat. + 20 + 15 + + + + + 6 + + + + + True + True + 20 + 15 + + + + + 7 + + + + + True + True + 20 + 15 + + + + + 8 + + + + + True + True + 20 + 15 + + + + + 9 + + + + + False + False + 1 + + + + + False + False + 0 + + + + + True + False + automatic + automatic + + + True + queue + + + 1000 + 1000 + True + True + True + GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK + + + + + + + + + + + + 1 + + + + + + + True + Linux Band + + + + False + tab + + + + + True + vertical + + + True + True + automatic + automatic + + + + + + 0 + + + + + 1 + + + + + True + MMA Source + + + + 1 + False + tab + + + + + 0 + + + + + True + True + bottom + 0 + 4 + 0 + + + + True + + + + + True + Graphical representation of the MMA file. Allows for basic editing. When switching from MMA Source -> Chordsheet view, the song gets compiled + Chordsheet + + + False + tab + + + + + True + + + 1 + + + + + True + MMA source code editor for power users + MMA Source + + + 1 + False + tab + + + + + False + 1 + + + + + False + True + + + + + 40 + True + True + automatic + automatic + + + True + True + False + word + + + + + True + True + + + + + 2 + + + + + + + 600 + 500 + GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK + Select Groove + True + True + False + mainWindow + + + + 600 + 500 + True + 0 + 0 + + + + 600 + 498 + True + + + True + 1 + + + True + True + automatic + automatic + + + True + True + 0 + False + + + + + + + + + 0 + + + + + True + True + automatic + automatic + + + True + True + + + + + + + 1 + + + + + True + vertical + + + True + 10 + 10 + 10 + 10 + + + True + True + automatic + automatic + + + True + False + False + word + + + + + + + 0 + + + + + + + + 40 + True + 5 + 5 + 5 + 5 + + + True + + + gtk-ok + True + True + True + True + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + gtk-cancel + True + True + True + True + + + + 2 + + + + + + + False + 2 + + + + + 35 + True + 5 + 5 + 5 + + + True + + + True + True + True + + + + True + gtk-go-back + + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + True + True + True + + + + True + gtk-go-forward + + + + + 2 + + + + + 5 + True + + + + + + False + 3 + + + + + gtk-delete + True + True + True + True + + + + 4 + + + + + + + False + 3 + + + + + 2 + + + + + + + + + 5 + Save the song as + center-on-parent + dialog + mainWindow + save + True + + + True + vertical + 2 + + + + + + True + end + + + gtk-save + -5 + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + -6 + True + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + + 5 + Open MMA file + center-on-parent + dialog + mainWindow + + + True + vertical + 2 + + + + + + True + end + + + gtk-open + -5 + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + -6 + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + + 200 + 150 + GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK + True + True + False + mainWindow + + + + True + 1 + 50 + + + + 200 + 150 + True + vertical + + + True + 5 + 5 + 5 + 5 + + + 20 + True + True + 3 + True + 5 + 120 60 250 1 10 0 + + + + + + 0 + + + + + True + 5 + 5 + 5 + 5 + + + True + + + gtk-ok + True + True + True + True + True + half + True + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + gtk-cancel + True + True + True + True + + + + 2 + + + + + + + False + 1 + + + + + 35 + True + 5 + 5 + 5 + + + True + + + True + True + True + + + + True + gtk-go-back + + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + True + True + True + + + + True + gtk-go-forward + + + + + 2 + + + + + 5 + True + + + + + + False + 3 + + + + + gtk-delete + True + True + True + True + + + + 4 + + + + + + + False + 2 + + + + + + + + + 200 + 50 + GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK + True + True + False + mainWindow + + + + True + 1 + 50 + 50 + + + + 200 + 50 + True + vertical + + + 45 + True + 10 + 5 + 5 + 5 + + + True + + + gtk-ok + True + True + True + True + True + half + True + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + True + True + True + + + + True + gtk-go-back + + + + + 2 + + + + + 5 + True + + + + + + False + 3 + + + + + True + True + True + + + + True + gtk-go-forward + + + + + 4 + + + + + 5 + True + + + + + + False + 5 + + + + + gtk-delete + True + True + True + True + + + + 6 + + + + + + + False + 0 + + + + + + + + + 200 + 120 + GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK + True + True + False + mainWindow + + + + True + 1 + 50 + 120 + + + + 200 + 115 + True + vertical + + + True + 5 + 5 + 5 + 5 + + + True + + + + + + + 0 + + + + + True + 5 + 5 + 5 + 5 + + + True + + + gtk-ok + True + True + True + True + True + half + True + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + gtk-cancel + True + True + True + True + + + + 2 + + + + + + + False + 1 + + + + + 35 + True + 5 + 5 + 5 + + + True + + + True + True + True + + + + True + gtk-go-back + + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + True + True + True + + + + True + gtk-go-forward + + + + + 2 + + + + + 5 + True + + + + + + False + 3 + + + + + gtk-delete + True + True + True + True + + + + 4 + + + + + + + False + 2 + + + + + + + + + 200 + 120 + GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK + True + True + False + mainWindow + + + + True + 1 + 50 + 120 + + + + 200 + 115 + True + vertical + + + True + 5 + 5 + 5 + 5 + + + True + + + + + + + 0 + + + + + True + 5 + 5 + 5 + 5 + + + True + + + gtk-ok + True + True + True + True + True + half + True + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + gtk-cancel + True + True + True + True + + + + 2 + + + + + + + False + 1 + + + + + 35 + True + 5 + 5 + 5 + + + True + + + True + True + True + + + + True + gtk-go-back + + + + + 0 + + + + + 5 + True + + + + + + False + 1 + + + + + True + True + True + + + + True + gtk-go-forward + + + + + 2 + + + + + 5 + True + + + + + + False + 3 + + + + + gtk-delete + True + True + True + True + + + + 4 + + + + + + + False + 2 + + + + + + + + + 5 + Save as MIDI + center-on-parent + normal + mainWindow + save + True + + + True + vertical + 2 + + + + + + True + end + + + gtk-save + -5 + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + -6 + True + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + + 5 + center-on-parent + audio-x-generic + normal + mainWindow + Program name + Version + (c) Year Author + audio-x-generic + + + + + True + vertical + 2 + + + + + + True + end + + + False + end + 0 + + + + + + + 300 + 5 + Preferences + True + center-on-parent + normal + mainWindow + True + + + True + vertical + 2 + + + True + 5 + 5 + 5 + 5 + + + True + vertical + + + True + 2 + + + True + 0 + Path to MMA + + + + + 0 + + + + + True + Full path to MMA executable. Usually /usr/bin/mma + False + + + False + 1 + + + + + True + 15 + 2 + + + True + 0 + Path to MMA grooves directory + + + + + 2 + + + + + True + Full path to grooves directory. Usually /usr/share/mma/lib/stdlib + select-folder + False + + + False + 3 + + + + + True + 15 + 2 + + + True + 0 + Chordsheet font + + + + + 4 + + + + + True + True + True + Font used to draw chord names on the chord sheet + True + False + + + 5 + + + + + True + 20 + 15 + + + Connect to JACK on startup + True + True + False + Starts the JACK client on Linux Band startup + True + + + + + 6 + + + + + + + 1 + + + + + True + end + + + gtk-apply + -5 + True + True + True + True + True + True + True + + + False + False + 0 + + + + + gtk-cancel + -6 + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + + 5 + True + center-on-parent + normal + True + mainWindow + warning + yes-no + + + True + vertical + 2 + + + True + end + + + + + + + + + False + end + 0 + + + + + + diff --git a/src/main/python/linuxband.py.in b/src/main/python/linuxband.py.in new file mode 100755 index 0000000..e366d45 --- /dev/null +++ b/src/main/python/linuxband.py.in @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import logging +import pygtk +pygtk.require("2.0") +import gobject + +# substituted by autoconf +PACKAGE_NAME = "@PACKAGE_NAME@" +PACKAGE_VERSION = "@PACKAGE_VERSION@" +PACKAGE_BUGREPORT = "@PACKAGE_BUGREPORT@" +PACKAGE_URL = "@PACKAGE_URL@" +PACKAGE_TITLE = "@PACKAGE_TITLE@" +PACKAGE_COPYRIGHT = "@PACKAGE_COPYRIGHT@" +PKG_DATA_DIR = "@pkgdatadir@" +PKG_LIB_DIR = "@pkglibdir@" + +# python version +PYTHON_MAJOR = 2 +PYTHON_MINOR = 5 + +def main(): + if not check_python_version(): + sys.exit(1) + gobject.threads_init() + sys.path.insert(0, PKG_DATA_DIR) + from linuxband.glob import Glob + Glob.PACKAGE_VERSION = PACKAGE_VERSION + Glob.PACKAGE_BUGREPORT = PACKAGE_BUGREPORT + Glob.PACKAGE_URL = PACKAGE_URL + Glob.PACKAGE_TITLE = PACKAGE_TITLE + Glob.PACKAGE_COPYRIGHT = PACKAGE_COPYRIGHT + Glob.LINE_MARKER = "%s/line-pointer.png" % PKG_DATA_DIR + Glob.ERROR_MARKER = "%s/error-pointer.png" % PKG_DATA_DIR + Glob.DEFAULT_CONFIG_FILE = "%s/linuxband.rc" % PKG_DATA_DIR + Glob.GLADE = "%s/gui.glade" % PKG_DATA_DIR + Glob.LICENSE = "%s/license.txt" % PKG_DATA_DIR + Glob.PLAYER_PROGRAM = "%s/linuxband-player" % PKG_LIB_DIR + # initialize logging + from linuxband.logger import Logger + console_log_level = logging.INFO + # enable debugging + if ('-d' in sys.argv[1:] or '--debug' in sys.argv[1:]): + console_log_level = logging.DEBUG + Glob.CONSOLE_LOG_LEVEL = console_log_level + Logger.initLogging(console_log_level) + logging.debug("%s %s" % (PACKAGE_NAME, PACKAGE_VERSION)) + # start the gui + from linuxband.gui.gui import Gui + Gui() + +def check_python_version(): + if sys.version_info[0] != PYTHON_MAJOR or sys.version_info[1] < PYTHON_MINOR: + print "This program requires Python version 2.x, where x >= %i" % PYTHON_MINOR + print "Found Python version %s" % sys.version + return False + return True + +if __name__ == "__main__": + main() diff --git a/src/main/python/linuxband/__init__.py b/src/main/python/linuxband/__init__.py new file mode 100644 index 0000000..c3245bf --- /dev/null +++ b/src/main/python/linuxband/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/src/main/python/linuxband/config.py b/src/main/python/linuxband/config.py new file mode 100644 index 0000000..b1c6433 --- /dev/null +++ b/src/main/python/linuxband/config.py @@ -0,0 +1,143 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import ConfigParser +import logging +from linuxband.glob import Glob +import os + + +class Config(object): + + __rc_file = Glob.CONFIG_DIR + '/linuxband.rc' + __home_dir = Glob.HOME_DIR + __default_config = Glob.DEFAULT_CONFIG_FILE + + __SAVED = "Saved" + __PREFERENCES = "Preferences" + __MMA_PATH = "mma_path" + __MMA_GROOVES_PATH = "mma_grooves_path" + __CHORD_SHEET_FONT = "chord_sheet_font" + __JACK_CONNECT_STARTUP = "jack_connect_startup" + __TEMPLATE_FILE = "template_file" + __WORK_DIR = "work_dir" + __LOOP = "loop" + __JACK_TRANSPORT = "jack_transport" + __INTRO_LENGTH = "intro_length" + + def __init__(self): + self.__config = ConfigParser.SafeConfigParser() + + def load_config(self): + if self.__ensure_dir(Glob.CONFIG_DIR): + try: + logging.debug("Opening config") + self.__config.read(Config.__rc_file) + except: + logging.exception("Error reading configuration file '%s', loading defaults." % Config.__rc_file) + self.__load_default_config() + else: + self.__load_default_config() + self.save_config() + + def save_config(self): + """ + Save configuration into file. + """ + fname = Config.__rc_file + logging.info("Saving configuration to '" + fname + "'") + try: + out_file = file(fname, 'w') + try: + self.__config.write(out_file) + finally: + out_file.close() + except: + logging.exception("Unable to save configuration file '" + fname + "'") + + def get_work_dir(self): + return self.__config.get(Config.__SAVED, Config.__WORK_DIR) + + def set_work_dir(self, work_dir): + self.__config.set(Config.__SAVED, Config.__WORK_DIR, work_dir) + + def get_jack_transport(self): + return self.__config.getboolean(Config.__SAVED, Config.__JACK_TRANSPORT) + + def set_jack_transport(self, value): + self.__config.set(Config.__SAVED, Config.__JACK_TRANSPORT, str(value)) + + def get_loop(self): + return self.__config.getboolean(Config.__SAVED, Config.__LOOP) + + def set_loop(self, value): + self.__config.set(Config.__SAVED, Config.__LOOP, str(value)) + + def get_intro_length(self): + return self.__config.getint(Config.__SAVED, Config.__INTRO_LENGTH) + + def set_intro_length(self, value): + self.__config.set(Config.__SAVED, Config.__INTRO_LENGTH, str(value)) + + def getTemplateFile(self): + return self.__config.get(Config.__PREFERENCES, Config.__TEMPLATE_FILE) + + def setTemplateFile(self, file_name): + self.__config.set(Config.__PREFERENCES, Config.__TEMPLATE_FILE, file_name) + + def get_mma_path(self): + return self.__config.get(Config.__PREFERENCES, Config.__MMA_PATH) + + def set_mma_path(self, mma_path): + self.__config.set(Config.__PREFERENCES, Config.__MMA_PATH, mma_path) + + def get_mma_grooves_path(self): + return self.__config.get(Config.__PREFERENCES, Config.__MMA_GROOVES_PATH) + + def set_mma_grooves_path(self, path): + self.__config.set(Config.__PREFERENCES, Config.__MMA_GROOVES_PATH, path) + + def get_jack_connect_startup(self): + return self.__config.getboolean(Config.__PREFERENCES, Config.__JACK_CONNECT_STARTUP) + + def set_jack_connect_startup(self, connect): + self.__config.set(Config.__PREFERENCES, Config.__JACK_CONNECT_STARTUP, str(connect)) + + def get_chord_sheet_font(self): + return self.__config.get(Config.__PREFERENCES, Config.__CHORD_SHEET_FONT) + + def set_chord_sheet_font(self, font): + self.__config.set(Config.__PREFERENCES, Config.__CHORD_SHEET_FONT, font) + + def __load_default_config(self): + try: + self.__config.read(Config.__default_config) + self.set_work_dir(Config.__home_dir) + except: + logging.exception("Failed to read default configuration from '" + Config.__default_config + "'") + + def __ensure_dir(self, newdir): + """ + If the directory already exists return True. Else try to create it and return False. + """ + if os.path.isdir(newdir): + return True + try: + os.mkdir(newdir) + except: + logging.exception("Unable to create directory '" + newdir + "'") + return False diff --git a/src/main/python/linuxband/glob.py b/src/main/python/linuxband/glob.py new file mode 100644 index 0000000..d73c4a1 --- /dev/null +++ b/src/main/python/linuxband/glob.py @@ -0,0 +1,63 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + + +class Glob: + + # action constants in alphabet order + A_AUTHOR = "AUTHOR" + A_BEGIN_BLOCK = "BEGINBLOCK" + A_DEF_GROOVE = "DEFGROOVE" + A_DOC = "DOC" + A_GROOVE = "GROOVE" + A_REMARK = "REMARK" # comment line with song title + A_REPEAT = "REPEAT" + A_REPEAT_END = "REPEATEND" + A_REPEAT_ENDING = "REPEATENDING" + A_TEMPO = "TEMPO" + A_TIME = "TIME" + A_TIME_SIG = "TIMESIG" + A_UNKNOWN = "UNKNOWN" + + # list of supported events, order of Add event menulist + EVENTS = [ A_GROOVE, A_TEMPO, A_REPEAT, A_REPEAT_ENDING, A_REPEAT_END ] + + OUTPUT_FILE_DEFAULT = "untitled.mma" + UNTITLED_SONG_NAME = "Untitled Song" + + # user's home dir - works for windows/unix/linux + HOME_DIR = os.getenv('USERPROFILE') or os.getenv('HOME') + CONFIG_DIR = HOME_DIR + '/.linuxband' + + PID = os.getpid() + + # will be set by main function + PACKAGE_VERSION = "" + PACKAGE_BUGREPORT = "" + PACKAGE_URL = "" + PACKAGE_TITLE = "" + PACKAGE_COPYRIGHT = "" + LINE_MARKER = "" + ERROR_MARKER = "" + DEFAULT_CONFIG_FILE = "" + GLADE = "" + LICENSE = "" + PLAYER_PROGRAM = "" + CONSOLE_LOG_LEVEL = None + diff --git a/src/main/python/linuxband/gui/__init__.py b/src/main/python/linuxband/gui/__init__.py new file mode 100644 index 0000000..c3245bf --- /dev/null +++ b/src/main/python/linuxband/gui/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/src/main/python/linuxband/gui/about_dialog.py b/src/main/python/linuxband/gui/about_dialog.py new file mode 100644 index 0000000..31ed53f --- /dev/null +++ b/src/main/python/linuxband/gui/about_dialog.py @@ -0,0 +1,53 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +from linuxband.gui.common import Common +from linuxband.glob import Glob + + +class AboutDialog(object): + + def __init__(self, glade): + dialog = self.__about_dialog = glade.get_widget("aboutDialog") + dialog.set_program_name(Glob.PACKAGE_TITLE) + dialog.set_version(Glob.PACKAGE_VERSION) + dialog.set_copyright(Glob.PACKAGE_COPYRIGHT + ', ' + Glob.PACKAGE_BUGREPORT) + dialog.set_website(Glob.PACKAGE_URL) + fname = Glob.LICENSE + license_text = '' + try: + infile = file(fname, 'r') + try: + license_text = infile.read() + finally: + infile.close() + except: + logging.exception("Unable to read license file '" + fname + "'") + dialog.set_license(license_text) + Common.connect_signals(glade, self) + + def help_about_callback(self, menuitem): + self.__about_dialog.present() + + def about_dialog_response_callback(self, dialog, response): + self.__about_dialog.hide() + return True + + def about_dialog_delete_event_callback(self, dialog, event): + self.__about_dialog.hide() + return True diff --git a/src/main/python/linuxband/gui/chord_entries.py b/src/main/python/linuxband/gui/chord_entries.py new file mode 100644 index 0000000..939766c --- /dev/null +++ b/src/main/python/linuxband/gui/chord_entries.py @@ -0,0 +1,186 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import gobject +from gtk.gdk import CONTROL_MASK +from linuxband.gui.common import Common +from linuxband.mma.chord_table import chordlist + + +class ChordEntries(object): + + def __init__(self, glade, song, chord_sheet): + self.__song = song + self.__completion_match = False + self.__init_gui(glade, self.__get_chord_names()) + self.__chord_sheet = chord_sheet + + def refresh(self): + """ Refresh chord entries. """ + if self.__chord_sheet.is_cursor_on_bar_info(): return + barnum = self.__chord_sheet.get_current_bar_number() + + for entry in self.__entries: + entry.set_text('') + + if barnum < self.__song.get_data().get_bar_count(): + chords = self.__song.get_data().get_bar_chords(barnum).get_chords() + for i in range(len(chords)): + chord = chords[i][0] + if chord == '/': # don't show '/' as chord + self.__entries[i].set_text('') + else: + self.__entries[i].set_text(chord) + else: + for i in range(self.__song.get_data().get_beats_per_bar()): + self.__entries[i].set_text('') + + def on_entry_key_release_event_callback(self, widget, event): + key = event.keyval + if key == gtk.keysyms.Escape \ + or (event.state & CONTROL_MASK and key == gtk.keysyms.bracketright): + # cancel chord editing + self.__chord_sheet.render_current_field() + self.refresh() + self.__chord_sheet.grab_focus() + return False + chords = [] + for i in range(self.__song.get_data().get_beats_per_bar()): + chords.append([self.__entries[i].get_text()]) + self.__chord_sheet.render_current_field(chords) + if key == gtk.keysyms.Return or key == gtk.keysyms.KP_Enter: + if self.__completion_match: + self.__completion_match = False + else: + self.__chord_sheet.chord_entry_editing_finished() + return False + + def on_entry_focus_out_event_callback(self, widget, event): + """ Set the chord. """ + barnum = self.__chord_sheet.get_current_bar_number() + beatnum = self.__entries.index(widget) + chord = widget.get_text() + self.__song.get_data().get_bar_chords(barnum).set_chord(beatnum, chord) + return False + + def begin_writing(self, char): + """ Called from ChordSheet when any of CDEFGAB has been pressed. """ + self.__entries[0].set_text(char) + self.__entries[0].grab_focus() + self.__entries[0].get_completion().complete() + self.__entries[0].select_region(-1, -1) + + def grab_focus(self, num): + gobject.idle_add(self.__entries[num].grab_focus) + + def has_focus(self): + return self.__find_focused_entry() != None + + def hide(self): + self.__hbox6.hide() + + def show(self): + self.__hbox6.show() + + def cut_selection(self): + entry = self.__find_focused_entry() + entry.cut_clipboard() + + def copy_selection(self): + entry = self.__find_focused_entry() + entry.copy_clipboard() + + def paste_selection(self): + entry = self.__find_focused_entry() + entry.paste_clipboard() + + def delete_selection(self): + entry = self.__find_focused_entry() + entry.delete_selection() + + def select_all(self): + entry = self.__find_focused_entry() + entry.select_region(0, -1) + + def __init_gui(self, glade, chord_names): + Common.connect_signals(glade, self) + self.__hbox6 = glade.get_widget("hbox6") + self.__entries = [ glade.get_widget("entry1"), + glade.get_widget("entry2"), + glade.get_widget("entry3"), + glade.get_widget("entry4"), + glade.get_widget("entry5"), + glade.get_widget("entry6"), + glade.get_widget("entry7"), + glade.get_widget("entry8") ] + # Initialize chord entry completion + model = gtk.ListStore(str) + for chord in chord_names: + model.append([chord]) + for entry in self.__entries: + completion = gtk.EntryCompletion() + completion.set_model(model) + completion.set_text_column(0) + completion.set_match_func(self.__match_function) + completion.connect("match-selected", self.__on_completion_match) + entry.set_completion(completion) + # hide redundant entries + for entry in self.__entries[self.__song.get_data().get_beats_per_bar():]: + entry.hide() + # create a focus cycle + focus_chain = [] + for entry in self.__entries[:self.__song.get_data().get_beats_per_bar()]: + focus_chain.append(entry) + focus_chain.append(self.__entries[0]) + self.__hbox6.set_focus_chain(focus_chain) + + def __get_chord_names(self): + """ + Generate a list with all possible chords. + + It is used for entry completion + """ + base = [ 'C', 'D', 'E', 'F', 'G', 'A', 'B' ] + base_ext = [] + for c in base: + base_ext.append(c) + base_ext.append(c + '#') + base_ext.append(c + 'b') + + chord_names = [] + for c in base_ext: + for k, v in chordlist.iteritems(): #@UnusedVariable + chord_names.append(c + k) + + chord_names.sort() + return chord_names + + def __match_function(self, completion, key, it): + # key is case-insensitive + model = completion.get_model() + key = completion.get_entry().get_text() + return model[it][0].startswith(key) + + def __on_completion_match(self, completion, model, it): + self.__completion_match = True + + def __find_focused_entry(self): + for entry in self.__entries: + if entry.is_focus(): + return entry + return None diff --git a/src/main/python/linuxband/gui/chord_sheet.py b/src/main/python/linuxband/gui/chord_sheet.py new file mode 100644 index 0000000..55c3ec1 --- /dev/null +++ b/src/main/python/linuxband/gui/chord_sheet.py @@ -0,0 +1,567 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import pango +import copy +from gtk.gdk import CONTROL_MASK, SHIFT_MASK, BUTTON1_MASK +import logging +from linuxband.mma.bar_chords import BarChords +from linuxband.mma.bar_info import BarInfo +from linuxband.gui.common import Common + + +class ChordSheet(object): + + __bar_height = 40 + __cell_padding = 2 + __max_bar_chords_font = 40 + __bars_per_line = 4 + + __color_no_song = "honeydew3" + __color_playhead = "black" + __color_selection = "SteelBlue" + __color_cursor = "yellow" + __color_events = "grey73" + __color_song = "honeydew2" + + def __init__(self, glade, song, gui, config): + self.__song = song + self.__cursor_pos = 0 + self.__end_position = 0 + self.__playhead_pos = -1 + self.__selection_start = None + self.__selection = set([]) + self.__clipboard = [] + self.__gui = gui + self.__config = config + self.__init_gui(glade) + + def drawing_area_realize_event_callback(self, widget): + self.drawable = self.__area.window + self.gc = self.drawable.new_gc() + # Create a new backing pixmap of the appropriate size + self.pixmap = gtk.gdk.Pixmap(self.drawable, self.__drawing_area_width, self.__drawing_area_height, depth= -1) + gc = self.drawable.new_gc() + gc.copy(self.gc) + green = self.__colormap.alloc_color(ChordSheet.__color_no_song, True, True) + gc.set_foreground(green) + self.pixmap.draw_rectangle(gc, True, 0, 0, self.__drawing_area_width, self.__drawing_area_height) + return True + + def drawing_area_expose_event_callback(self, widget, event): + """ Redraw the screen from the backing pixmap. """ + x , y, width, height = event.area + widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL], + self.pixmap, x, y, x, y, width, height) + return True + + def move_playhead_to(self, pos): + new_pos = pos * 2 + 1 if pos > -1 else -1 + old_pos = self.__playhead_pos + if old_pos != new_pos: + self.__playhead_pos = new_pos + self.__render_field(old_pos) + self.__render_field(new_pos) + + def drawing_area_keypress_event_callback(self, widget, event): + key = event.keyval + old_pos = self.__cursor_pos + if key == gtk.keysyms.Left or key == gtk.keysyms.h or key == gtk.keysyms.H: + targetPos = self.__cursor_pos - 1 + if targetPos >= 0: + self.__move_cursor_to(targetPos) + elif key == gtk.keysyms.Right or key == gtk.keysyms.l or key == gtk.keysyms.L: + self.__move_cursor_to(self.__cursor_pos + 1) + elif key == gtk.keysyms.Up or key == gtk.keysyms.k or key == gtk.keysyms.K: + targetPos = self.__cursor_pos - ChordSheet.__bars_per_line * 2 + if targetPos >= 0: + self.__move_cursor_to(targetPos) + elif key == gtk.keysyms.Down or key == gtk.keysyms.j or key == gtk.keysyms.J: + self.__move_cursor_to(self.__cursor_pos + ChordSheet.__bars_per_line * 2) + elif key == gtk.keysyms.Home: + self.__move_cursor_to(0) + elif key == gtk.keysyms.End: + self.__move_cursor_to(self.__end_position) + elif event.state & CONTROL_MASK and key == gtk.keysyms.Delete: + self.cut_selection() + elif event.state & CONTROL_MASK and key == gtk.keysyms.Insert: + self.copy_selection() + elif event.state & SHIFT_MASK and key == gtk.keysyms.Insert: + self.paste_selection() + elif key == gtk.keysyms.Delete: + self.delete_selection() + # move focus to chord entries + elif self.__is_bar_chords(self.__cursor_pos): + if key == gtk.keysyms.Return: + self.__move_cursor_to(self.__cursor_pos + 2) + self.__destroy_selection() + # adjust selection + if key in [ gtk.keysyms.Left, gtk.keysyms.Right, gtk.keysyms.Up, gtk.keysyms.Down, + gtk.keysyms.Home, gtk.keysyms.End ]: + if event.state & SHIFT_MASK: + self.__adjust_selection(old_pos) + else: + self.__destroy_selection() + if key in [ gtk.keysyms.H, gtk.keysyms.L, gtk.keysyms.K, gtk.keysyms.J ]: + self.__adjust_selection(old_pos) + if key in [ gtk.keysyms.h, gtk.keysyms.l, gtk.keysyms.k, gtk.keysyms.j ]: + self.__destroy_selection() + # the event has been handled + return True + + def cut_selection(self): + """ Put selected fields from area into clipboard and delete them. """ + self.copy_selection() + self.delete_selection() + + def copy_selection(self): + """ Copy selected fields from area into clipboard. """ + if len(self.__selection) == 0: + sel = set([self.__cursor_pos]) + else: + sel = self.__selection + self.__clipboard = [] + for field_num in sorted(sel): + bar_num = field_num / 2 + if self.__is_bar_chords(field_num): + self.__clipboard.append(copy.deepcopy(self.__song.get_data().get_bar_chords(bar_num))) + else: + self.__clipboard.append(copy.deepcopy(self.__song.get_data().get_bar_info(bar_num))) + + def paste_selection(self): + """ Paste fields from the clipboard. """ + clipboard = list(self.__clipboard) + length = len(clipboard) + if length > 0: + if isinstance(clipboard[0], BarChords) and self.__is_bar_info(self.__cursor_pos): + logging.warning("cannot paste chords on bar-info") + return + elif isinstance(clipboard[0], BarInfo) and self.__is_bar_chords(self.__cursor_pos): + logging.warning("cannot paste bar-info on chords") + return + self.__destroy_selection() + old_pos = self.__cursor_pos + new_pos = old_pos + length - 1 + self.__move_cursor_to(new_pos) # if needed new fields will be appended + bar_num = old_pos / 2 + for field in clipboard: + if isinstance(field, BarChords): + self.__song.get_data().set_bar_chords(bar_num, field) + bar_num = bar_num + 1 + else: + self.__song.get_data().set_bar_info(bar_num, field) + self.__adjust_selection(old_pos) # refreshes affected fields + if old_pos > 0: self.__render_field(old_pos - 1) # refresh barNumber + + def delete_selection(self): + """ Delete selected fields """ + if len(self.__selection) == 0: + sel = set([self.__cursor_pos]) + else: + sel = self.__selection + for field_num in sorted(sel): + bar_num = field_num / 2 + if self.__is_bar_chords(field_num): + self.__song.get_data().set_bar_chords(bar_num, self.__song.get_data().create_bar_chords()) + else: + self.__song.get_data().set_bar_info(bar_num, self.__song.get_data().create_bar_info()) + # refresh deleted fields + possible bar_info at the beginning + for field_num in sel: + self.__render_field(field_num) + m = min(sel) + if m > 0: self.__render_field(m - 1) + # if deleted bars from the end, shorten the song + if max(sel) == self.__end_position: + first = min(sel) + if self.__is_bar_info(first): + if (first + 1) in sel: + first = first + 1 + if self.__is_bar_chords(first): + bar_num = first / 2 + self.__gui.change_song_bar_count(bar_num) + self.__refresh_entries_and_events() + + def select_all(self): + self.__move_cursor_to(self.__end_position) + self.__adjust_selection(0) + + def get_selection_limits(self): + """ Selection start and end bar numbers """ + if self.__selection: + return min(self.__selection) / 2, max(self.__selection) / 2 + else: + return self.__cursor_pos / 2, self.__cursor_pos / 2 + + def drawing_area_clicked(self, widget, event): + if event.button == 1: + old_pos = self.__cursor_pos + pos = self.__locate_mouse_click(event.x, event.y) + self.__move_cursor_to(pos) + if event.state & SHIFT_MASK: + self.__adjust_selection(old_pos) + else: + self.__destroy_selection() + self.__area.grab_focus() + + def drawing_area_motion_notify_event_callback(self, widget, event): + # selection by mouse + if event.state & BUTTON1_MASK: + pos = self.__locate_mouse_click(event.x, event.y) + old_pos = self.__cursor_pos + if not pos == old_pos: + self.__move_cursor_to(pos) + self.__adjust_selection(old_pos) + return False + + def grab_focus(self): + self.__area.grab_focus() + + def has_focus(self): + return self.__area.is_focus() + + def change_song_bar_count(self, bar_count): + new_end = bar_count * 2 + if new_end == self.__end_position: + return + elif new_end > self.__end_position: # render affected fields + fields_to_render = range(self.__end_position, new_end + 1) + for field in fields_to_render: self.__render_field(field) + elif new_end < self.__end_position: + fields_to_render = range(new_end, self.__end_position + 1) + for field in fields_to_render: self.__render_field(field) + self.__end_position = new_end + + if new_end == 0: + self.__cursor_pos = 0 + elif self.__cursor_pos > new_end: + self.__cursor_pos = new_end + + self.__move_cursor_to(self.__cursor_pos) + self.__adjust_selection_bar_count_changed() + + def set_song_bar_count(self, bar_count): + new_end = bar_count * 2 + maxlast = max(self.__end_position, new_end) + fields_to_render = range(0, maxlast + 1) + for field in fields_to_render: self.__render_field(field) + self.__end_position = new_end + + if new_end == 0: + self.__cursor_pos = 0 + elif self.__cursor_pos > new_end: + self.__cursor_pos = new_end + + self.__move_cursor_to(self.__cursor_pos) + self.__adjust_selection_bar_count_changed() + + def is_cursor_on_bar_info(self): + return self.__is_bar_info(self.__cursor_pos) + + def is_cursor_on_bar_chords(self): + return self.__is_bar_chords(self.__cursor_pos) + + def get_current_bar_number(self): + return self.__cursor_pos / 2 + + def new_song_loaded(self): + self.__move_cursor_to(0) # cursor on field 0 => global buttons get refreshed + self.__destroy_selection() + + def render_current_field(self, chords=None): + self.__render_field(self.__cursor_pos, chords) + + def chord_entry_editing_finished(self): + self.grab_focus() + self.__move_cursor_to(self.__cursor_pos + 2) + self.__destroy_selection() + + def __init_gui(self, glade): + Common.connect_signals(glade, self) + self.__area = glade.get_widget("drawingarea1") + self.__colormap = self.__area.get_colormap() + self.__drawing_area_width, self.__drawing_area_height = self.__area.get_size_request() + self.__bar_width = self.__drawing_area_width / self.__song.get_data().get_beats_per_bar() + self.__bar_chords_width = self.__bar_width * 9 / 10 + self.__bar_info_width = self.__bar_width - self.__bar_chords_width + self.__area.show() + + def __render_chord_xy(self, chord, x, y, width, height, playhead): + """ Render one chord on position x,y. """ + if playhead: color = self.__colormap.alloc_color('white') + else: color = self.__colormap.alloc_color('black') + gc = self.pixmap.new_gc() + gc.copy(self.gc) + #gc.set_line_attributes(1, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND) + gc.set_foreground(color) + #self.pixmap.draw_rectangle(gc, False, x , y, width, height) + + pango_layout = self.__area.create_pango_layout("") + pango_layout.set_text(chord) + fd = pango.FontDescription(self.__config.get_chord_sheet_font()) + + size = (ChordSheet.__max_bar_chords_font + 1) * pango.SCALE + while True: + size = size - pango.SCALE + fd.set_size(size) + pango_layout.set_font_description(fd) + text_width, text_height = pango_layout.get_pixel_size() + if text_width <= width and text_height <= height: + break + + ink, logical = pango_layout.get_pixel_extents() #@UnusedVariable + self.pixmap.draw_layout(gc, x, y + (height - ink[1] - ink[3]), pango_layout) + + def __render_chords_xy(self, bar_num, chords, bar_x, bar_y, playhead, cursor, selection): + if bar_num >= self.__song.get_data().get_bar_count(): + color_code = ChordSheet.__color_no_song + elif playhead: + color_code = ChordSheet.__color_playhead + elif selection: + color_code = ChordSheet.__color_selection + elif cursor: + color_code = ChordSheet.__color_cursor + else: + color_code = ChordSheet.__color_song + + color = self.__colormap.alloc_color(color_code) + gc = self.pixmap.new_gc() + gc.copy(self.gc) + gc.set_foreground(color) + self.pixmap.draw_rectangle(gc, True, bar_x , bar_y, self.__bar_chords_width, ChordSheet.__bar_height) + if cursor: # black border + color = self.__colormap.alloc_color("black") + gc.set_foreground(color) + self.pixmap.draw_rectangle(gc, False, bar_x , bar_y, self.__bar_chords_width - 1, ChordSheet.__bar_height - 1) + + if not chords: + return + + if playhead: + color = self.__colormap.alloc_color("white") + else: + color = self.__colormap.alloc_color("black") + gc.set_foreground(color) + + bar_chords_width = self.__bar_chords_width - (self.__song.get_data().get_beats_per_bar()) * ChordSheet.__cell_padding + bar_chords_height = ChordSheet.__bar_height - 2 * ChordSheet.__cell_padding + chord_width = bar_chords_width / self.__song.get_data().get_beats_per_bar() + i = 0 + while i < len(chords): + if chords[i][0] == '/': + i = i + 1 + continue + # there is a chord on this beat + if i % 2 == 0 \ + and i + 1 < self.__song.get_data().get_beats_per_bar() \ + and (i + 1 >= len(chords) or chords[i + 1][0] == '/' or chords[i + 1][0] == ''): + # the next beat has no chord we can expand us + self.__render_chord_xy(chords[i][0], + bar_x + (chord_width + ChordSheet.__cell_padding) * i, + bar_y + ChordSheet.__cell_padding, + chord_width + ChordSheet.__cell_padding + chord_width, + bar_chords_height, + playhead) + else: + self.__render_chord_xy(chords[i][0], + bar_x + (chord_width + ChordSheet.__cell_padding) * i, + bar_y + ChordSheet.__cell_padding, + chord_width, + bar_chords_height, + playhead) + i = i + 1 + + def __draw_repetition(self, x, y, gc, end): + # draw line + middle_x = x + self.__bar_info_width / 2 + start_y = y + ChordSheet.__cell_padding + width = ChordSheet.__cell_padding + height = ChordSheet.__bar_height - 2 * ChordSheet.__cell_padding + self.pixmap.draw_rectangle(gc, True, middle_x, start_y, width, height) + # draw points + point_size = ChordSheet.__cell_padding * 2 + upper_y = y + ChordSheet.__bar_height / 4 + lower_y = y + ChordSheet.__bar_height * 3 / 4 - point_size + if end: x = x + self.__bar_info_width / 5 + else: x = x + self.__bar_info_width * 4 / 5 - point_size + self.pixmap.draw_arc(self.gc, True, x, upper_y, point_size, point_size, 0, 360 * 64) + self.pixmap.draw_arc(self.gc, True, x, lower_y, point_size, point_size, 0, 360 * 64) + + def __render_bar_info_xy(self, bar_num, chord_num, bar_info, x, y, cursor, selection): + if bar_num > self.__song.get_data().get_bar_count(): + color_code = ChordSheet.__color_no_song + elif selection: + color_code = ChordSheet.__color_selection + elif cursor: + color_code = ChordSheet.__color_cursor + elif bar_info.has_events(): + color_code = ChordSheet.__color_events + else: + color_code = ChordSheet.__color_song + + color = self.__colormap.alloc_color(color_code) + + gc = self.drawable.new_gc() + gc.copy(self.gc) + gc.set_foreground(color) + self.pixmap.draw_rectangle(gc, True, x , y, self.__bar_info_width, ChordSheet.__bar_height) + if cursor: # black border + color = self.__colormap.alloc_color('black') + gc.set_foreground(color) + self.pixmap.draw_rectangle(gc, False, x, y, self.__bar_info_width - 1, ChordSheet.__bar_height - 1) + + color = self.__colormap.alloc_color('black') + gc.set_foreground(color) + repeat_begin = bar_info.has_repeat_begin() if bar_info else False + repeat_end = bar_info.has_repeat_end() if bar_info else False + if repeat_begin or repeat_end: # draw repetitions + if repeat_begin: self.__draw_repetition(x, y, gc, False) + if repeat_end: self.__draw_repetition(x, y, gc, True) + else: + if bar_num < self.__song.get_data().get_bar_count() and chord_num: # draw bar number + pango_layout = self.__area.create_pango_layout("") + pango_layout.set_text(str(chord_num)) + fd = pango.FontDescription('Monospace Bold 8') + pango_layout.set_font_description(fd) + ink, logical = pango_layout.get_pixel_extents() #@UnusedVariable + self.pixmap.draw_layout(gc, x + ChordSheet.__cell_padding, + y + (ChordSheet.__bar_height - ink[1] - ink[3]) - ChordSheet.__cell_padding, pango_layout) + + def __get_pos_x(self, pos): + return (pos / 2 % ChordSheet.__bars_per_line) * self.__bar_width \ + + (pos % 2 * self.__bar_info_width) + + def __get_pos_y(self, pos): + return (pos / 2 / ChordSheet.__bars_per_line) * ChordSheet.__bar_height + + def __is_bar_info(self, field_num): + return field_num % 2 == 0 + + def __is_bar_chords(self, field_num): + return field_num % 2 == 1 + + def __render_field(self, field_num, chords=None): + cursor = self.__cursor_pos == field_num + playhead = self.__playhead_pos == field_num + selection = field_num in self.__selection + + field_x = self.__get_pos_x(field_num) + field_y = self.__get_pos_y(field_num) + bar_num = field_num / 2 + + if self.__is_bar_chords(field_num): + if chords == None and bar_num < self.__song.get_data().get_bar_count(): + chords = self.__song.get_data().get_bar_chords(bar_num).get_chords() + + self.__render_chords_xy(bar_num, chords, field_x, field_y, playhead, cursor, selection) + self.__area.queue_draw_area(field_x, field_y, self.__bar_chords_width, ChordSheet.__bar_height) + else: + bar_info = None + if bar_num <= self.__song.get_data().get_bar_count(): + bar_info = self.__song.get_data().get_bar_info(bar_num) + + chord_num = None + if bar_num < self.__song.get_data().get_bar_count(): + chord_num = self.__song.get_data().get_bar_chords(bar_num).get_number() + + self.__render_bar_info_xy(bar_num, chord_num, bar_info, field_x, field_y, cursor, selection) + self.__area.queue_draw_area(field_x, field_y, self.__bar_info_width, ChordSheet.__bar_height) + + def __refresh_entries_and_events(self): + self.__gui.refresh_bar(self.is_cursor_on_bar_chords()) + + def __move_cursor_to(self, new_pos): + old_pos = self.__cursor_pos + self.__cursor_pos = new_pos + + self.__render_field(old_pos) + + if new_pos > self.__end_position: + new_song_bar_count = new_pos / 2 + new_pos % 2 + self.__gui.change_song_bar_count(new_song_bar_count) + + self.__render_field(new_pos) + + self.__gui.switch_bar(self.is_cursor_on_bar_chords()) + self.__refresh_entries_and_events() + + def __redraw_selection(self, old_sel): + removed = old_sel - self.__selection + added = self.__selection - old_sel + update = removed | added + for field in update: + self.__render_field(field) + + def __get_selection_set(self, start, end): + if end >= start: + return set(range(start, end + 1)) + else: + return set(range(end, start + 1)) + + def __adjust_selection(self, old_pos): + """ Begins or adjusts field selection, redraws affected fields. """ + if self.__selection_start == None: + # beginning a new selection + self.__selection_start = old_pos + self.__selection = self.__get_selection_set(old_pos, self.__cursor_pos) + self.__redraw_selection(set([])) + else: + # adjust already existing selection + old_selection = self.__selection + self.__selection = self.__get_selection_set(self.__selection_start, self.__cursor_pos) + self.__redraw_selection(old_selection) + + def __adjust_selection_bar_count_changed(self): + """ Number of bars has changed, maybe destroy the selection or shorten it. """ + if self.__selection_start == None: + return + elif self.__selection_start > self.__end_position: + self.__destroy_selection() + elif max(self.__selection) > self.__end_position: + # remove all fields from selection which are out of song + self.__selection = set([elem for elem in self.__selection if elem <= self.__end_position]) + + def __destroy_selection(self): + if self.__selection_start == None: return + old_selection = self.__selection + self.__selection_start = None + self.__selection = set([]) + self.__redraw_selection(old_selection) + + def __locate_mouse_click(self, x, y): + bar_x = 0 + bar_chords = 0 + u = self.__bar_info_width + while x > u: + if bar_chords == 0: + u = u + self.__bar_chords_width + bar_chords = 1 + else: + u = u + self.__bar_info_width + bar_x = bar_x + 1 + bar_chords = 0 + + bar_y = 0 + u = ChordSheet.__bar_height + while y > u: + u = u + ChordSheet.__bar_height + bar_y = bar_y + 1 + + pos = self.__song.get_data().get_beats_per_bar() * 2 * bar_y + bar_x * 2 + bar_chords + + return pos + diff --git a/src/main/python/linuxband/gui/common.py b/src/main/python/linuxband/gui/common.py new file mode 100644 index 0000000..3cc921d --- /dev/null +++ b/src/main/python/linuxband/gui/common.py @@ -0,0 +1,30 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import inspect + + +class Common(object): + + @staticmethod + def connect_signals(glade, obj): + dicts = {} + for name, member in inspect.getmembers(obj): + dicts[name] = member + glade.signal_autoconnect(dicts) + + diff --git a/src/main/python/linuxband/gui/events/__init__.py b/src/main/python/linuxband/gui/events/__init__.py new file mode 100644 index 0000000..c3245bf --- /dev/null +++ b/src/main/python/linuxband/gui/events/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/src/main/python/linuxband/gui/events/groove.py b/src/main/python/linuxband/gui/events/groove.py new file mode 100644 index 0000000..13a12e9 --- /dev/null +++ b/src/main/python/linuxband/gui/events/groove.py @@ -0,0 +1,128 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import pango +import gobject +from linuxband.glob import Glob +from linuxband.mma.bar_info import BarInfo +from linuxband.gui.common import Common + + +class EventGroove(object): + + __GROOVE_UNDEFINED = "Select groove" + + def __init__(self, glade, grooves): + self.__toggled_button = None + self.__curr_event = None + self.__new_event = None + self.__grooves = grooves + self.__init_gui(glade) + + def on_treeview1_cursor_changed_callback(self, treeview): + """ User clicked on the groove in the first column. """ + path, column = treeview.get_cursor() #@UnusedVariable + gr = self.__groovesModel[path[0]] + self.__treeview2.set_model(gr[6]) + self.__update_groove_info(gr) + self.__update_groove_event(gr[0]) + + def on_treeview2_cursor_changed_callback(self, treeview): + """ User clicked on the variation of groove. """ + path, column = self.__treeview1.get_cursor() #@UnusedVariable + model = self.__groovesModel[path[0]][6] + path, column = self.__treeview2.get_cursor() #@UnusedVariable + gr = model[path[0]] + self.__update_groove_info(gr) + self.__update_groove_event(gr[0]) + + def set_label_from_event(self, button, event): + """ Sets the label of groove button correctly + even if the event in the self.__song.get_data().get_bar_info(0) is missing. """ + if event: button.set_label(BarInfo.get_groove_value(event)) + else: button.set_label(EventGroove.__GROOVE_UNDEFINED) + + def get_new_event(self): + return self.__new_event + + def init_window(self, button, event): + # hide back, forward, remove buttons + if button is self.__togglebutton1: self.__alignment12.hide() + else: self.__alignment12.show() + self.__toggled_button = button + self.__curr_event = event + self.__new_event = None + # if the focus stayed on the button, put it to first column + if not self.__treeview1.is_focus() and not self.__treeview2.is_focus(): + gobject.idle_add(self.__treeview1.grab_focus) + self.__groovesModel = self.__grooves.get_grooves_model() + self.__treeview1.set_model(self.__groovesModel) + + def __init_gui(self, glade): + Common.connect_signals(glade, self) + # back, forward, remove event buttons will be hidden + self.__alignment12 = glade.get_widget("alignment12") + # groove description + groove_window = glade.get_widget("grooveWindow") + textview2 = glade.get_widget("textview2") + self.__textbuffer2 = textview2.get_buffer() + # text colors + colormap = groove_window.get_colormap() + color = colormap.alloc_color('red') + self.__textbuffer2.create_tag('fg_red', foreground_gdk=color) + color = colormap.alloc_color('brown'); + self.__textbuffer2.create_tag('fg_brown', foreground_gdk=color) + color = colormap.alloc_color('black'); + self.__textbuffer2.create_tag('fg_black', foreground_gdk=color) + self.__textbuffer2.create_tag("bold", weight=pango.WEIGHT_BOLD) + self.__togglebutton1 = glade.get_widget("togglebutton1") + # groove columns + self.__treeview1 = glade.get_widget("treeview1") + self.__treeview2 = glade.get_widget("treeview2") + # grooves column + tvcolumn = gtk.TreeViewColumn('Groove') + self.__treeview1.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, True) + tvcolumn.set_attributes(cell, text=0) + # groove variation column + self.__treeview2.set_model(None) + tvcolumn = gtk.TreeViewColumn('Variation') + self.__treeview2.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, True) + tvcolumn.set_attributes(cell, text=0) + + def __update_groove_info(self, gr): + """ Update description, author ... of the currently selected groove. """ + tb = self.__textbuffer2 + tb.set_text('') + start, end = tb.get_bounds() #@UnusedVariable + tb.insert_with_tags_by_name(end, gr[0] + '\n', 'fg_brown', 'bold') + tb.insert(end, gr[1] + '\n\n') + tb.insert_with_tags_by_name(end, gr[2] + '\n\n', 'bold') + tb.insert_with_tags_by_name(end, 'Author' + '\n', 'fg_brown') + tb.insert(end, gr[3] + '\n\n') + tb.insert_with_tags_by_name(end, 'File' + '\n', 'fg_brown') + tb.insert(end, gr[5] + '\n\n') + + def __update_groove_event(self, groove): + event = BarInfo.create_event(Glob.A_GROOVE) + BarInfo.set_groove_value(event, groove) + self.set_label_from_event(self.__toggled_button, event) + self.__new_event = event diff --git a/src/main/python/linuxband/gui/events/repeat.py b/src/main/python/linuxband/gui/events/repeat.py new file mode 100644 index 0000000..d6d581b --- /dev/null +++ b/src/main/python/linuxband/gui/events/repeat.py @@ -0,0 +1,31 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +class EventRepeat(object): + + def __init__(self, glade): + pass + + def set_label_from_event(self, button, event): + """ Sets the label of Repeat button. """ + button.set_label("Repeat") + + def get_new_event(self): + return None + + def init_window(self, button, data): + pass diff --git a/src/main/python/linuxband/gui/events/repeat_end.py b/src/main/python/linuxband/gui/events/repeat_end.py new file mode 100644 index 0000000..83a0172 --- /dev/null +++ b/src/main/python/linuxband/gui/events/repeat_end.py @@ -0,0 +1,73 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +from linuxband.glob import Glob +from linuxband.mma.bar_info import BarInfo +from linuxband.gui.common import Common + + +class EventRepeatEnd(object): + + def __init__(self, glade): + self.__toggled_button = None + self.__curr_event = None + self.__init_gui(glade) + + def on_comboboxentry1_changed_callback(self, widget): + """ RepeatEnd value changed. """ + # called also when initializing the entries list + if not self.__toggled_button: return + try: + i = int(widget.child.get_text()) + except ValueError: + return + count = str(i) + event = BarInfo.create_event(Glob.A_REPEAT_END) + BarInfo.set_repeat_end_value(event, count) + self.set_label_from_event(self.__toggled_button, event) + self.__newEvent = event + + def set_label_from_event(self, button, event): + """ Sets the label of RepeatEnd button when the count has changed. """ + count = BarInfo.get_repeat_end_value(event) + if count == "2": label = "RepeatEnd" + else: label = "RepeatEnd " + count + button.set_label(label) + + def get_new_event(self): + return self.__newEvent + + def init_window(self, button, event): + self.__toggled_button = button + self.__curr_event = event + self.__newEvent = None + text = BarInfo.get_repeat_end_value(event) + self.__entry.set_text(text) + self.__combobox_entry.grab_focus() + + def __init_gui(self, glade): + Common.connect_signals(glade, self) + # RepeatEnd entry + self.__combobox_entry = glade.get_widget("comboboxentry1") + self.__entry = self.__combobox_entry.child + combobox = self.__combobox_entry + list_store = gtk.ListStore(str) + combobox.set_model(list_store) + for i in range(2, 6): + combobox.append_text(str(i)) + combobox.set_active(0) diff --git a/src/main/python/linuxband/gui/events/repeat_ending.py b/src/main/python/linuxband/gui/events/repeat_ending.py new file mode 100644 index 0000000..a500f0a --- /dev/null +++ b/src/main/python/linuxband/gui/events/repeat_ending.py @@ -0,0 +1,74 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +from linuxband.glob import Glob +from linuxband.mma.bar_info import BarInfo +from linuxband.gui.common import Common + + +class EventRepeatEnding(object): + + def __init__(self, glade): + self.__toggled_button = None + self.__curr_event = None + self.__new_event = None + self.__init_gui(glade) + + def on_comboboxentry2_changed_callback(self, widget): + """ RepeatEnding value changed """ + # called also when initializing the entries list + if not self.__toggled_button: return + try: + i = int(widget.child.get_text()) + except ValueError: + return + count = str(i) + event = BarInfo.create_event(Glob.A_REPEAT_ENDING) + BarInfo.set_repeat_ending_value(event, count) + self.set_label_from_event(self.__toggled_button, event) + self.__new_event = event + + def set_label_from_event(self, button, event): + """ Sets the label of RepeatEnding button when the count was changed """ + count = BarInfo.get_repeat_ending_value(event) + if count == "2": label = "RepeatEnding" + else: label = "RepeatEnding " + count + button.set_label(label) + + def get_new_event(self): + return self.__new_event + + def init_window(self, button, event): + self.__toggled_button = button + self.__curr_event = event + self.__new_event = None + text = BarInfo.get_repeat_ending_value(event) + self.__entry.set_text(text) + self.__combobox_entry.grab_focus() + + def __init_gui(self, glade): + Common.connect_signals(glade, self) + # RepeatEnding entry + self.__combobox_entry = glade.get_widget("comboboxentry2") + self.__entry = self.__combobox_entry.child + combobox = self.__combobox_entry + list_store = gtk.ListStore(str) + combobox.set_model(list_store) + for i in range(2, 6): + combobox.append_text(str(i)) + combobox.set_active(0) diff --git a/src/main/python/linuxband/gui/events/tempo.py b/src/main/python/linuxband/gui/events/tempo.py new file mode 100644 index 0000000..1c004c2 --- /dev/null +++ b/src/main/python/linuxband/gui/events/tempo.py @@ -0,0 +1,71 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from linuxband.glob import Glob +from linuxband.mma.bar_info import BarInfo +from linuxband.gui.common import Common + + +class EventTempo(object): + + __TEMPO_UNDEFINED = "Select tempo" + + def __init__(self, glade): + self.__toggled_button = None + self.__curr_event = None + self.__new_event = None + self.__init_gui(glade) + + def on_spinbutton2_value_changed_callback(self, spinbutton): + """ Tempo changed """ + count = str(int(spinbutton.get_value())) + event = BarInfo.create_event(Glob.A_TEMPO) + BarInfo.set_tempo_value(event, count) + self.set_label_from_event(self.__toggled_button, event) + self.__new_event = event + + def set_label_from_event(self, button, event): + """ Set the label of tempo button correctly + even if the event in the self.__song.get_data().get_bar_info(0) is missing """ + if event: + label = BarInfo.get_tempo_value(event) + ' BPM' + button.set_label(label) + else: + button.set_label(EventTempo.__TEMPO_UNDEFINED) + + def get_new_event(self): + return self.__new_event + + def init_window(self, button, event): + # hide back, forward, remove buttons + if button is self.__togglebutton2: self.__alignment15.hide() + else: self.__alignment15.show() + self.__toggled_button = button + self.__curr_event = event + self.__new_event = None + if event: + self.__spinbutton2.set_value(int(BarInfo.get_tempo_value(event))) + else: + self.on_spinbutton2_value_changed_callback(self.__spinbutton2) + self.__spinbutton2.grab_focus() + + def __init_gui(self, glade): + Common.connect_signals(glade, self) + # back, forward, remove event buttons will be hidden + self.__alignment15 = glade.get_widget("alignment15") + self.__togglebutton2 = glade.get_widget("togglebutton2") + self.__spinbutton2 = glade.get_widget("spinbutton2") diff --git a/src/main/python/linuxband/gui/events_bar.py b/src/main/python/linuxband/gui/events_bar.py new file mode 100644 index 0000000..5330212 --- /dev/null +++ b/src/main/python/linuxband/gui/events_bar.py @@ -0,0 +1,334 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gobject +import gtk +from linuxband.glob import Glob +from linuxband.gui.common import Common +from linuxband.gui.events.groove import EventGroove +from linuxband.gui.events.repeat import EventRepeat +from linuxband.gui.events.repeat_end import EventRepeatEnd +from linuxband.gui.events.repeat_ending import EventRepeatEnding +from linuxband.gui.events.tempo import EventTempo +from linuxband.mma.bar_info import BarInfo + + +class EventsBar(object): + + def __init__(self, glade, song, gui, grooves): + self.__song = song + # triple = [ button, window, handler, event ] + self.__triples = [] + # triple which window is currently active + self.__toggled_triple = None + # toggleWindowClose workaround + self.__toggle_window_close_recursive = False + # original label on toggle button just before opening the window + self.__toggled_button_label = None + # event which is being edited in the toggleWindow + self.__curr_event = None + self.__grooves = grooves + self.__gui = gui + self.__init_gui(glade) + + def move_event_backwards_callback(self, widget): + """ Move events button and it's window to the left. """ + box = self.__hbox8 + index = box.child_get_property(self.__toggled_triple[0], "position") + if index > 0: + # move event in the model + barNum = self.__gui.get_current_bar_number() + self.__song.get_data().get_bar_info(barNum).move_event_backwards(self.__curr_event) + # move the button and its window + box.reorder_child(self.__toggled_triple[0], index - 1) + gobject.idle_add(self.__move_window_underneath, self.__toggled_triple[1], self.__toggled_triple[0]) + # we could have moved the tempo event to the beginning -> became the global tempo + self.__refresh_globals() + + def move_event_forwards_callback(self, widget): + """ Move events button and it's window to the right. """ + box = self.__hbox8 + index = box.child_get_property(self.__toggled_triple[0], "position") + if index < len(box.get_children()) - 1: + # move event in the model + barNum = self.__gui.get_current_bar_number() + self.__song.get_data().get_bar_info(barNum).move_event_forwards(self.__curr_event) + # move the button and its window + box.reorder_child(self.__toggled_triple[0], index + 1) + gobject.idle_add(self.__move_window_underneath, self.__toggled_triple[1], self.__toggled_triple[0]) + # the global tempo or the global groove could have been changed + self.__refresh_globals() + + def remove_event_callback(self, widget): + """ Remove the event from event bar. """ + # remove event from the model + barNum = self.__gui.get_current_bar_number() + self.__song.get_data().get_bar_info(barNum).remove_event(self.__curr_event) + # remove the window from the triples list + self.__remove_from_triples(self.__toggled_triple[0]) + # remove button and close the window + self.__hbox8.remove(self.__toggled_triple[0]) + self.__toggle_window_close() # deletes the self.__toggled_triple + self.__refresh_globals() + self.__gui.refresh_current_field() # repetition + + def add_event_clicked_callback(self, button): + """ Add event button pressed. """ + menu = self.__addEventMenu + x, y = self.__get_widget_xy_position(button, menu) + self.__toggle_window_close() + event = gtk.get_current_event() + menu.popup(None, None, lambda menu: (x, y, True), event.button, event.time) + + def toggle_button_clicked_callback(self, button, event=None): + """ User clicked on toggle button (global button or event button). """ + if button.get_active(): + # button is pressed in + if self.__toggled_triple: + # some other button is toggled + self.__toggle_window_close() + for triple in self.__triples: + if triple[0] is button: break + self.__toggled_button_label = button.get_label() + if button is self.__togglebutton1: + self.__curr_event = self.__song.get_data().get_bar_info(0).get_groove() + elif button is self.__togglebutton2: + self.__curr_event = self.__song.get_data().get_bar_info(0).get_tempo() + else: + self.__curr_event = event + self.__move_window_underneath(triple[1], button) + triple[2].init_window(button, self.__curr_event) + triple[1].show() + self.__toggled_triple = triple + else: + # button is released + self.__toggle_window_close() + + def toggle_window_expose_event_callback(self, widget, event): + self.__draw_window_border(widget) + return False + + def toggle_window_cancel_callback(self, widget): + """ Toggle window cancel button clicked. """ + self.__toggle_window_close(True) + + def toggle_window_ok_callback(self, widget, *args): + """ Toggle window OK button clicked. """ + # we didn't touch the settings, don't set label + newEvent = self.__toggled_triple[2].get_new_event() + button = self.__toggled_triple[0] + if not newEvent: + self.__toggle_window_close(True) + else: + self.__toggle_window_close(False) + if button in [self.__togglebutton1, self.__togglebutton2]: + if not self.__curr_event: self.__song.get_data().get_bar_info(0).add_event(newEvent) + else: self.__song.get_data().get_bar_info(0).replace_event(self.__curr_event, newEvent) + else: + barNum = self.__gui.get_current_bar_number() + self.__song.get_data().get_bar_info(barNum).replace_event(self.__curr_event, newEvent) + self.refresh_all() # global groove or global tempo could have been changed + + def main_window_configure_event_callback(self, widget, event): + """ Everytime the main window is moved or resised, move the toggle window with it. """ + if self.__toggled_triple: + self.__move_window_underneath(self.__toggled_triple[1], self.__toggled_triple[0]) + return False + + def toggle_window_key_pressed_event_callback(self, widget, event): + """ If the Escape was pressed, close the toggle window. """ + key = event.keyval + if key == gtk.keysyms.Escape: + self.__toggle_window_close(True) + return True + + def refresh_all(self): + """ Refresh global buttons Select groove, Select tempo and event buttons. """ + self.__refresh_globals() + self.__refresh_events_bar() + + def grab_focus(self): + """ First event button or Add event button takes focus. """ + if len(self.__triples) > 0: + self.__triples[0][0].grab_focus() + else: + self.__button17.grab_focus() + + def hide(self): + self.__hbox7.hide() + + def show(self): + self.__hbox7.show() + + def __init_gui(self, glade): + Common.connect_signals(glade, self) + self.__main_window = glade.get_widget("mainWindow") + groove_window = glade.get_widget("grooveWindow") + tempo_window = glade.get_widget("tempoWindow") + repeat_window = glade.get_widget("repeatWindow") + repeat_end_window = glade.get_widget("repeatEndWindow") + repeat_ending_window = glade.get_widget("repeatEndingWindow") + self.__hbox8 = glade.get_widget("hbox8") + self.__hbox7 = glade.get_widget("hbox7") + # add event button + self.__button17 = glade.get_widget("button17") + # global groove and tempo buttons + self.__togglebutton1 = glade.get_widget("togglebutton1") + self.__togglebutton2 = glade.get_widget("togglebutton2") + # global buttons + self.__eventGroove = EventGroove(glade, self.__grooves) + self.__eventTempo = EventTempo(glade) + self.__triples.append([ self.__togglebutton1, groove_window, self.__eventGroove, None ]) + self.__triples.append([ self.__togglebutton2, tempo_window, self.__eventTempo, None ]) + # add event menu + event_items = { Glob.A_GROOVE: "Groove change", + Glob.A_TEMPO: "Tempo change", + Glob.A_REPEAT: "Repeat", + Glob.A_REPEAT_ENDING: "RepeatEnding", + Glob.A_REPEAT_END: "RepeatEnd" } + self.__addEventMenu = gtk.Menu() + menu = self.__addEventMenu + for key in Glob.EVENTS: + item = gtk.MenuItem(event_items[key]) + item.connect_object("activate", self.__add_event, key) + menu.append(item) + menu.show_all() + # for dynamic event creation + self.__event_windows = { Glob.A_GROOVE: groove_window, + Glob.A_TEMPO: tempo_window, + Glob.A_REPEAT: repeat_window, + Glob.A_REPEAT_ENDING: repeat_ending_window, + Glob.A_REPEAT_END: repeat_end_window } + + self.__event_window_handlers = { Glob.A_GROOVE: self.__eventGroove, # reusing already existing object + Glob.A_TEMPO: self.__eventTempo, + Glob.A_REPEAT: EventRepeat(glade), + Glob.A_REPEAT_ENDING: EventRepeatEnding(glade), + Glob.A_REPEAT_END: EventRepeatEnd(glade) } + + def __refresh_events_bar(self): + """ Refresh events bar """ + if self.__gui.is_cursor_on_bar_chords(): return + barNum = self.__gui.get_current_bar_number() + box = self.__hbox8 + children = box.get_children() + # remove widgets from triples and events bar + for child in children: + self.__remove_from_triples(child) + box.remove(child) + # recreate event buttons + events = self.__song.get_data().get_bar_info(barNum).get_events() + for event in events: + title = event[0] + button = gtk.ToggleButton() + window = self.__event_windows[title] + handler = self.__event_window_handlers[title] + self.__triples.append([button, window, handler, event]) + handler.set_label_from_event(button, event) + button.connect("clicked", self.toggle_button_clicked_callback, event) + box.pack_start(button, False, False, 0) + button.show() + + def __add_event(self, key): + """ Add selected event and open its window """ + barNum = self.__gui.get_current_bar_number() + event = BarInfo.create_event(key) + self.__song.get_data().get_bar_info(barNum).add_event(event) + self.refresh_all() + self.__gui.refresh_current_field() # repetition + # open the new event window + for triple in self.__triples: + if triple[3] is event: + gobject.idle_add(triple[0].clicked) + break + + def __remove_from_triples(self, button): + for triple in self.__triples: + if triple[0] is button: break + if triple[0] is button: + self.__triples.remove(triple) + + def __move_window_underneath(self, gtkwindow, widget): + """ Move window to appear directly under the toggled button """ + rect = widget.get_allocation() + rect2 = self.__main_window.get_allocation() # (0, 0, 1100, 700) + + # black magic to get the correct values into rect3 + gtkwindow.realize() + gtkwindow.window.get_root_origin() + + rect3 = gtkwindow.get_allocation() + + # this has a side effect that x, y on the following line are set properly + x1, y1 = self.__main_window.window.get_root_origin() # decoration window @UnusedVariable + x, y = self.__main_window.window.get_origin() # our window + + wx = min(x + rect.x, x + rect2.width - rect3.width) + wy = y + rect.y + rect.height + + gtkwindow.move(wx, wy) + + def __get_widget_xy_position(self, widget, gtkwindow): + #""" Move window to appear directly under the toggle button """ + rect = widget.get_allocation() + rect2 = self.__main_window.get_allocation() # (0, 0, 1100, 700) + + # black magic to get the correct values into rect3 + gtkwindow.realize() + gtkwindow.window.get_root_origin() + + rect3 = gtkwindow.get_allocation() + + # this has a side effect that x, y on the following line are set properly + x1, y1 = self.__main_window.window.get_root_origin() # decoration window @UnusedVariable + x, y = self.__main_window.window.get_origin() # our window + + wx = min(x + rect.x, x + rect2.width - rect3.width) + wy = y + rect.y #+ rect.height + + return (wx, wy) + + def __draw_window_border(self, widget): + """ toggle windows have no decoration, we draw the border ourselvtoggleWindowClose(es """ + window = widget.bin_window + g = window.get_geometry() + width = g[2] + height = g[3] + gc = window.new_gc() + brown = widget.get_colormap().alloc_color('#AAAAA3') + gc.set_foreground(brown) + window.draw_rectangle(gc, False, 0, 0, width - 1, height - 1) + + def __toggle_window_close(self, restoreLabel=True): + """ Close toggle window """ + if self.__toggle_window_close_recursive: return + if self.__toggled_triple: + if restoreLabel: self.__toggled_triple[0].set_label(self.__toggled_button_label) + self.__toggled_triple[1].hide() + # this will invoke on_togglebutton_clicked and then this method recursive again! + self.__toggle_window_close_recursive = True + self.__toggled_triple[0].set_active(False) + self.__toggle_window_close_recursive = False + self.__toggled_triple = None + + def __refresh_globals(self): + """ Sets the global groove and global tempo labels """ + if self.__gui.get_current_bar_number() == 0: + groove = self.__song.get_data().get_bar_info(0).get_groove() + self.__eventGroove.set_label_from_event(self.__togglebutton1, groove) + tempo = self.__song.get_data().get_bar_info(0).get_tempo() + self.__eventTempo.set_label_from_event(self.__togglebutton2, tempo) diff --git a/src/main/python/linuxband/gui/gui.py b/src/main/python/linuxband/gui/gui.py new file mode 100644 index 0000000..1ec0935 --- /dev/null +++ b/src/main/python/linuxband/gui/gui.py @@ -0,0 +1,548 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pango +import logging +import gtk.glade +import gobject +from linuxband.glob import Glob +from linuxband.gui.gui_logger import GuiLogger +from linuxband.config import Config +from linuxband.midi.midi_player import MidiPlayer +from linuxband.gui.chord_sheet import ChordSheet +from linuxband.gui.source_editor import SourceEditor +from linuxband.gui.chord_entries import ChordEntries +from linuxband.gui.events_bar import EventsBar +from linuxband.gui.common import Common +from linuxband.mma.song import Song +from linuxband.mma.grooves import Grooves +from linuxband.gui.about_dialog import AboutDialog +from linuxband.midi.mma2smf import MidiGenerator +from linuxband.gui.preferences import Preferences +from linuxband.gui.save_button_status import SaveButtonStatus + + +class Gui: + + def application_end_event_callback(self, *args): + self.__do_application_end() + return True + + def switch_bar(self, on_bar_chords): + """ + If we are not on chords field hide the chord entries and show events. + """ + if on_bar_chords: + self.__chord_entries.show() + self.__events_bar.hide() + else: + self.__events_bar.show() + self.__chord_entries.hide() + + def refresh_bar(self, on_bar_chords): + """ Refresh chord_entries or events_bar. """ + if on_bar_chords: + self.__chord_entries.refresh() + else: + self.__events_bar.refresh_all() + + def refresh_current_field(self): + self.__chord_sheet.render_current_field() + + def refresh_chord_sheet(self): + self.__set_song_bar_count(self.__song.get_data().get_bar_count()) + + def is_cursor_on_bar_chords(self): + return self.__chord_sheet.is_cursor_on_bar_chords() + + def get_current_bar_number(self): + return self.__chord_sheet.get_current_bar_number() + + def move_playhead_to_bar(self, barNum): + """ Called by MidiPlayer. """ + self.__chord_sheet.move_playhead_to(barNum) + + def move_playhead_to_line(self, lineNum): + """ Called by MidiPlayer. """ + self.__source_editor.move_playhead_to(lineNum) + + def hide_playhead(self): + self.__chord_sheet.move_playhead_to(-1) + self.__source_editor.move_playhead_to(-1) + + def main_window_keypress_event_callback(self, widget, event): + key = event.keyval + keyname = gtk.gdk.keyval_name(key) + # print 'KEY %s %s' % (key, keyname) + if keyname in '12345678': + if self.__chord_sheet.has_focus() and self.__chord_sheet.is_cursor_on_bar_chords(): + if int(keyname) <= self.__song.get_data().get_beats_per_bar(): + self.__chord_entries.grab_focus(int(keyname) - 1) + return True + elif keyname in 'CDEFGAB': + if self.__chord_sheet.has_focus() and self.__chord_sheet.is_cursor_on_bar_chords(): + self.__chord_entries.begin_writing(keyname) + return True + elif keyname == 'e' and self.__chord_sheet.has_focus(): # TODO: doesn't work + self.__events_bar.grab_focus() + return True + elif (self.__chord_sheet.has_focus() or self.__notebook3.has_focus()) and keyname == 'space': + if (self.__midi_player.is_playing()): + self.playback_stop_callback() + else: + self.playback_start(None, event) + return True + # propagate the event further + return False + + def edit_cut_callback(self, menuitem): + if self.__chord_sheet.has_focus(): + self.__chord_sheet.cut_selection() + elif self.__source_editor.has_focus(): + self.__source_editor.cut_selection() + elif self.__chord_entries.has_focus(): + self.__chord_entries.cut_selection() + + def edit_copy_callback(self, menuitem): + if self.__chord_sheet.has_focus(): + self.__chord_sheet.copy_selection() + elif self.__source_editor.has_focus(): + self.__source_editor.copy_selection() + elif self.__chord_entries.has_focus(): + self.__chord_entries.copy_selection() + + def edit_paste_callback(self, menuitem): + if self.__chord_sheet.has_focus(): + self.__chord_sheet.paste_selection() + elif self.__source_editor.has_focus(): + self.__source_editor.paste_selection() + elif self.__chord_entries.has_focus(): + self.__chord_entries.paste_selection() + + def edit_delete_callback(self, menuitem): + if self.__chord_sheet.has_focus(): + self.__chord_sheet.delete_selection() + elif self.__source_editor.has_focus(): + self.__source_editor.delete_selection() + elif self.__chord_entries.has_focus(): + self.__chord_entries.delete_selection() + + def edit_select_all_callback(self, menuitem): + if self.__chord_sheet.has_focus(): + self.__chord_sheet.select_all() + elif self.__source_editor.has_focus(): + self.__source_editor.select_all() + elif self.__chord_entries.has_focus(): + self.__chord_entries.select_all() + + def view_preferences_callback(self, menuitem): + """ Preferences. """ + self.__preferences.run() + + __ignore_toggle2 = False + def switch_view_callback(self, item=None): + if Gui.__ignore_toggle2: + Gui.__ignore_toggle2 = False + else: + if self.__menuitem5.get_active(): + self.__notebook3.set_current_page(0) + else: + self.__notebook3.set_current_page(1) + + def spinbutton_keyrelease_event_callback(self, widget, event): + """ For songBarCount and introLength spin buttons """ + key = event.keyval + if key == gtk.keysyms.Return or key == gtk.keysyms.KP_Enter or key == gtk.keysyms.Escape: + self.__chord_sheet.grab_focus() + return True + + def song_bar_count_value_changed_callback(self, widget): + self.change_song_bar_count(widget.get_value_as_int()) + + def intro_length_value_changed_callback(self, widget): + length = widget.get_value_as_int() + self.__midi_player.set_intro_length(length) + self.__config.set_intro_length(length) + + def chord_sheet_button_press_event_callback(self, widget, event): + self.__events_bar.toggle_window_cancel_callback(None) + self.__chord_sheet.drawing_area_clicked(widget, event) + return False + + def change_song_bar_count(self, bar_count): + """ + Set the count of song bars to bar_count. + + Redraw affected fields + Move cursor if it is necessary + """ + self.__spinbutton1.set_text(str(bar_count)) + self.__song.get_data().change_bar_count(bar_count) + self.__chord_sheet.change_song_bar_count(bar_count) + + def song_title_keyrelease_event_callback(self, widget, event): + """ Finish Song title editing. """ + key = event.keyval + if key == gtk.keysyms.Return or key == gtk.keysyms.KP_Enter: + self.__song.get_data().set_title(widget.get_text()) + self.__chord_sheet.grab_focus() + return True + + def new_file_callback(self, menuitem): + """ New. """ + if self.__handle_unsaved_changes(): + self.__do_new_file() + + def open_file_callback(self, menutime): + """ Open. """ + if self.__handle_unsaved_changes(): + if (self.__open_file_dialog.get_current_folder() <> self.__config.get_work_dir()): + self.__open_file_dialog.set_current_folder(self.__config.get_work_dir()) + result = self.__open_file_dialog.run() + self.__open_file_dialog.hide() + if (result == gtk.RESPONSE_OK): + self.__config.set_work_dir(self.__open_file_dialog.get_current_folder()) + full_name = self.__open_file_dialog.get_filename() + manager = gtk.recent_manager_get_default() + manager.add_item('file://' + full_name) + self.__input_file = full_name + self.__output_file = full_name + self.__do_open_file() + + def open_recent_file(self, widget): + if self.__handle_unsaved_changes(): + uri = widget.get_current_item().get_uri() + file_name = uri[7:] + manager = gtk.recent_manager_get_default() + manager.add_item('file://' + file_name) + self.__input_file = file_name + self.__output_file = file_name + self.__do_open_file() + + def save_file_callback(self, menuitem): + """ Save. """ + self.__do_save_file() + + def save_file_as_callback(self, menuitem): + """ Save as. """ + self.__do_save_as() + + def export_midi_callback(self, menuitem): + """ Export MIDI. """ + if self.__compile_song(True) == 0: + if (self.__export_midi_dialog.get_current_folder() <> self.__config.get_work_dir()): + self.__export_midi_dialog.set_current_folder(self.__config.get_work_dir()) + out_file = self.__output_file if self.__output_file else Glob.OUTPUT_FILE_DEFAULT + out_file = self.__change_extension(out_file, "mid") + logging.debug(out_file) + self.__export_midi_dialog.set_current_name(out_file) + result = self.__export_midi_dialog.run() + self.__export_midi_dialog.hide() + if (result == gtk.RESPONSE_OK): + self.__config.set_work_dir(self.__export_midi_dialog.get_current_folder()) + full_name = self.__export_midi_dialog.get_filename() + self.__song.write_to_midi_file(full_name) + else: + logging.error("Failed to compile MMA file. Fix the errors and try the export again.") + + def playback_start(self, button, event): + """ Play. """ + if event.state & gtk.gdk.SHIFT_MASK and event.state & gtk.gdk.CONTROL_MASK: + self.__compile_song(True) + else: + player = self.__midi_player + res = self.__compile_song(True) + if res == 0: + res, midi_data = self.__song.get_playback_midi_data() + player.playback_stop() + if res != 0: # generate SMF failed + if res > 0 or res == -1: self.__show_mma_error(res) + return + player.load_smf_data(midi_data, self.__song.get_data().get_mma_line_offset()) + else: + return + self.__enable_pause_button(); + if event.state & gtk.gdk.CONTROL_MASK: + player.playback_start_bar(self.__chord_sheet.get_current_bar_number()) + elif event.state & gtk.gdk.SHIFT_MASK: + player.playback_start_bars(self.__chord_sheet.get_selection_limits()) + else: + player.playback_start() + + def playback_stop_callback(self, button=None): + """ Stop. """ + self.__enable_pause_button(); + self.__midi_player.playback_stop() + + __ignore_toggle = False + def playback_pause_callback(self, button=None): + """ Pause. """ + if Gui.__ignore_toggle: + Gui.__ignore_toggle = False + else: + self.__midi_player.set_pause(button.get_active()); + + def jack_reconnect_callback(self, button): + self.__midi_player.shutdown() + self.__midi_player.startup(); + + def switch_page_callback(self, notebook, page, pageNum): + """ Called when clicked on notebook tab. """ + logging.debug("") + if pageNum == 1: # switching to source editor + self.__source_editor.refresh_source(self.__song.write_to_string()) + self.__source_editor.grab_focus() + self.__notebook2.set_current_page(pageNum) + # view menu item + if not self.__menuitem7.get_active(): + Gui.__ignore_toggle2 = True + self.__menuitem7.set_active(True) + self.__global_buttons.hide() + else: # switching to chord sheet + res = self.__compile_song(True) + if res > 0 or res == -1: + logging.error("Cannot switch to chord sheet view. Fix the errors and try again.") + else: + self.refresh_chord_sheet() + self.__refresh_song_title() + self.__events_bar.refresh_all() + self.__notebook2.set_current_page(pageNum) + # view menu item + if not self.__menuitem5.get_active(): + Gui.__ignore_toggle2 = True + self.__menuitem5.set_active(True) + self.__global_buttons.show() + + def loop_toggle_callback(self, button): + """ Loop check button. """ + self.__midi_player.set_loop(button.get_active()); + self.__config.set_loop(button.get_active()) + + def jack_transport_toggle_callback(self, button): + """ Use JACK transport check button. """ + self.__midi_player.use_jack_transport(button.get_active()) + self.__config.set_jack_transport(button.get_active()) + + def __enable_pause_button(self): + if self.__toolbutton3.get_active(): + Gui.__ignore_toggle = True + self.__toolbutton3.set_active(False) + + def __do_application_end(self): + if self.__handle_unsaved_changes(): + # stop the jack thread when exiting + if self.__midi_player: + self.__midi_player.shutdown() + self.__config.save_config() + gtk.main_quit() + + def __set_song_bar_count(self, bar_count): + self.__spinbutton1.set_text(str(bar_count)) + self.__chord_sheet.set_song_bar_count(bar_count) + + def __refresh_song_title(self): + self.__entry9.set_text(self.__song.get_data().get_title()) + + def __do_new_file(self): + self.__output_file = None + self.__input_file = self.__config.getTemplateFile() + self.__do_open_file() + + def __do_open_file(self): + self.playback_stop_callback() + self.__song.load_from_file(self.__input_file) + res = self.__song.compile_song() + self.__chord_sheet.new_song_loaded() + self.__source_editor.new_song_loaded(self.__song.write_to_string()) + self.refresh_chord_sheet() + self.__refresh_song_title() + if res > 0 or res == -1: self.__show_mma_error(res) + + def __do_save_as(self): + if (self.__save_as_dialog.get_current_folder() <> self.__config.get_work_dir()): + self.__save_as_dialog.set_current_folder(self.__config.get_work_dir()) + self.__save_as_dialog.set_current_name(Glob.OUTPUT_FILE_DEFAULT) + result = self.__save_as_dialog.run() + self.__save_as_dialog.hide() + if (result == gtk.RESPONSE_OK): + self.__config.set_work_dir(self.__save_as_dialog.get_current_folder()) + full_name = self.__save_as_dialog.get_filename() + self.__compile_song(False) + self.__song.write_to_mma_file(full_name) + self.__output_file = full_name + return True + else: + return False + + def __do_save_file(self): + if self.__output_file == None: + return self.__do_save_as() + else: + self.__compile_song(False) + self.__song.write_to_mma_file(self.__output_file) + return True + + def __handle_unsaved_changes(self): + if self.__song.get_data().is_save_needed(): + self.__save_changes_dialog.set_property("text", "Save changes to " + self.__song.get_data().get_title() + "?") + self.__save_changes_dialog.set_property("secondary_text", "Your changes will be lost if you don't save them.") + result = self.__save_changes_dialog.run() + self.__save_changes_dialog.hide() + if (result == gtk.RESPONSE_YES): + return self.__do_save_file() + return True + + def __change_extension(self, file_name, ext): + name_and_extension = file_name.rsplit('.', 1) + if len(name_and_extension) == 2: + return ''.join([name_and_extension[0], '.', ext]) + else: + return file_name + + def __compile_song(self, show_error): + logging.debug("COMPILE_SONG") + res = self.__song.compile_song() + if show_error: + if res == 0: + self.__source_editor.put_error_mark_to(-1) + elif res > 0 or res == -1: + self.__show_mma_error(res) + return res + + def __show_mma_error(self, lineNum): + self.__source_editor.put_error_mark_to(lineNum - 1) + self.__notebook2.set_current_page(1) + gobject.idle_add(self.__notebook3.set_current_page, 1) + self.__source_editor.grab_focus() + + def __init_recent_menu(self, glade): + # code here comes from http://lescannoniers.blogspot.com/2008/11/pygtk-recent-file-chooser.html + # add a recent files menu item + manager = gtk.recent_manager_get_default() + # define a RecentChooserMenu object + recent_menu_chooser = gtk.RecentChooserMenu(manager) + # define a file filter, otherwise all file types will show up + file_filter = gtk.RecentFilter() + file_filter.add_pattern("*.mma") + # add the file_filter to the RecentChooserMenu object + recent_menu_chooser.add_filter(file_filter) + recent_menu_chooser.set_sort_type(gtk.RECENT_SORT_MRU) + recent_menu_chooser.set_limit(10) + recent_menu_chooser.set_show_tips(True) + recent_menu_chooser.set_show_numbers(True) + recent_menu_chooser.set_local_only(True) + # add a signal to open the selected file + recent_menu_chooser.connect("item-activated", self.open_recent_file) + # attach the RecentChooserMenu to the main menu item + menuitem6 = glade.get_widget("menuitem6") + menuitem6.set_submenu(recent_menu_chooser) + # attach the RecentChooserMenu to the Open tool button + toolbutton8 = glade.get_widget("toolbutton8") + toolbutton8.set_menu(recent_menu_chooser) + + def __init_filechooser_dialogs(self, glade): + self.__open_file_dialog = glade.get_widget("openFileDialog") + self.__save_as_dialog = glade.get_widget("saveAsDialog") + self.__export_midi_dialog = glade.get_widget("exportMidiDialog") + # set file filters for Open and Save as dialogs + filter1 = gtk.FileFilter() + filter1.set_name("MMA files") + filter1.add_pattern("*.mma") + filter2 = gtk.FileFilter() + filter2.set_name("All files") + filter2.add_pattern("*") + filter3 = gtk.FileFilter() + filter3.set_name("MIDI files") + filter3.add_pattern("*.mid") + self.__open_file_dialog.add_filter(filter1) + self.__open_file_dialog.add_filter(filter2) + self.__save_as_dialog.add_filter(filter1) + self.__save_as_dialog.add_filter(filter2) + self.__export_midi_dialog.add_filter(filter2) + self.__export_midi_dialog.add_filter(filter3) + + def __init__(self): + glade = gtk.glade.XML(Glob.GLADE) + GuiLogger.initLogging(glade) + Common.connect_signals(glade, self) + + self.__main_window = glade.get_widget("mainWindow") + self.__spinbutton1 = glade.get_widget("spinbutton1") # bar count + self.__notebook2 = glade.get_widget("notebook2") + self.__notebook3 = glade.get_widget("notebook3") + + # song name + self.__entry9 = glade.get_widget("entry9") + self.__entry9.modify_font(pango.FontDescription('10')) + + # global buttons + self.__global_buttons = glade.get_widget("vbox10") + + # pause button + self.__toolbutton3 = glade.get_widget("toolbutton3") + + # view menu toggle + self.__menuitem5 = glade.get_widget("menuitem5") + self.__menuitem7 = glade.get_widget("menuitem7") + + # hack to get event object when clicked on toolbutton + toolbutton1 = glade.get_widget("toolbutton1") + toolbutton1.get_children()[0].connect('button-press-event', self.playback_start) + + # save changes dialog + self.__save_changes_dialog = glade.get_widget("saveChangesDialog") + + self.__config = Config() + self.__config.load_config() + grooves = Grooves(self.__config) + grooves.load_grooves(True) + + self.__song = song = Song(MidiGenerator(self.__config)) + self.__chord_sheet = ChordSheet(glade, song, self, self.__config) + self.__events_bar = EventsBar(glade, song, self, grooves) + self.__chord_entries = ChordEntries(glade, song, self.__chord_sheet) + self.__source_editor = SourceEditor(glade, song) + self.__preferences = Preferences(glade, self, self.__config, grooves) + + AboutDialog(glade) + SaveButtonStatus(glade, song) + + self.__init_recent_menu(glade) + self.__init_filechooser_dialogs(glade) + + self.__main_window .show() + + gobject.threads_init() + self.__midi_player = MidiPlayer(self) + if (self.__config.get_jack_connect_startup()): + self.__midi_player.startup() + + # loop check button, must be after the __midiPlayer.startup() call + checkbutton1 = glade.get_widget("checkbutton1") + checkbutton1.set_active(self.__config.get_loop()) + + # jack transport check button, must be after the __midiPlayer.startup() call + checkbutton3 = glade.get_widget("checkbutton3") + checkbutton3.set_active(self.__config.get_jack_transport()) + + # intro length, , must be after the __midiPlayer.startup() call + spinbutton3 = glade.get_widget("spinbutton3") + spinbutton3.set_value(self.__config.get_intro_length()) + + self.__do_new_file() + gtk.main() + diff --git a/src/main/python/linuxband/gui/gui_logger.py b/src/main/python/linuxband/gui/gui_logger.py new file mode 100644 index 0000000..881cfbf --- /dev/null +++ b/src/main/python/linuxband/gui/gui_logger.py @@ -0,0 +1,84 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import traceback + + +class GuiLogger(object): + + class __TextBufferHandler(logging.Handler): + + def __init__(self, textView, textBuffer): + logging.Handler.__init__(self) + self.__textView = textView + self.__textBuffer = textBuffer + + def emit(self, record): + record.exc_text = None # is needed to formatException to be called + + if record.levelname == 'INFO': tag = 'fg_black' + elif record.levelname == 'WARNING': tag = 'fg_brown' + elif record.levelname == 'ERROR': tag = 'fg_red' + + start, end = self.__textBuffer.get_bounds() #@UnusedVariable + text = self.__textBuffer.get_text(start, end) + eol = '' if text == '' else '\n' + + mark = self.__textBuffer.create_mark(None, end, False) + self.__textBuffer.insert_with_tags_by_name(end, eol + self.format(record), tag) + self.__textView.scroll_to_mark(mark, 0, True, 0.0, 1.0) + self.__textBuffer.delete_mark(mark) + + class __MyFormatter(logging.Formatter): + + def __init__(self, fmt=None, datefmt=None): + logging.Formatter.__init__(self, fmt, datefmt) + + def formatException(self, ei): + excType, excValue, excTraceback = ei #@UnusedVariable + res = ''.join(traceback.format_exception_only(excType, excValue)) + if res[-1] == '\n': + res = res[:-1] + return res + + @staticmethod + def initLogging(glade): + textView = glade.get_widget("textview1") + textBuffer = textView.get_buffer() + GuiLogger.__createColorTags(textView, textBuffer) + GuiLogger.__configureLogging(textView, textBuffer) + + @staticmethod + def __createColorTags(textView, textBuffer): + colormap = textView.get_colormap() + color = colormap.alloc_color('red') + textBuffer.create_tag('fg_red', foreground_gdk=color) + color = colormap.alloc_color('brown'); + textBuffer.create_tag('fg_brown', foreground_gdk=color) + color = colormap.alloc_color('black'); + textBuffer.create_tag('fg_black', foreground_gdk=color) + + @staticmethod + def __configureLogging(textView, textBuffer): + guiFormat = "%(asctime)s %(levelname)s %(message)s" + dateFormat = "%H:%M:%S" + textBufferHandler = GuiLogger.__TextBufferHandler(textView, textBuffer) + textBufferHandler.setLevel(logging.INFO) + textBufferHandler.setFormatter(GuiLogger.__MyFormatter(guiFormat, dateFormat)) + rootLogger = logging.getLogger(); + rootLogger.addHandler(textBufferHandler) diff --git a/src/main/python/linuxband/gui/preferences.py b/src/main/python/linuxband/gui/preferences.py new file mode 100644 index 0000000..2409aa1 --- /dev/null +++ b/src/main/python/linuxband/gui/preferences.py @@ -0,0 +1,65 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from linuxband.gui.common import Common +import gtk + +class Preferences(object): + + def __init__(self, glade, gui, config, grooves): + self.__gui = gui + self.__config = config + self.__grooves = grooves + self.__init_gui(glade) + + def run(self): + self.__initWidgets(); + result = self.__preferencesdialog.run() + self.__preferencesdialog.hide() + if (result == gtk.RESPONSE_OK): + self.__apply_changes() + self.__config.save_config() + + def __init_gui(self, glade): + Common.connect_signals(glade, self) + self.__preferencesdialog = glade.get_widget("preferencesDialog") + self.__filechooserbutton1 = glade.get_widget("filechooserbutton1") + self.__filechooserbutton2 = glade.get_widget("filechooserbutton2") + self.__fontbutton1 = glade.get_widget("fontbutton1") + self.__checkbutton2 = glade.get_widget("checkbutton2") + + def __initWidgets(self): + self.__filechooserbutton1.set_filename(self.__config.get_mma_path()) + self.__filechooserbutton2.set_filename(self.__config.get_mma_grooves_path()) + self.__fontbutton1.set_font_name(self.__config.get_chord_sheet_font()) + self.__checkbutton2.set_active(self.__config.get_jack_connect_startup()) + + def __apply_changes(self): + # path to mma + self.__config.set_mma_path(self.__filechooserbutton1.get_filename()) + # path to grooves + new_mma_grooves_path = self.__filechooserbutton2.get_filename() + if (self.__config.get_mma_grooves_path() != new_mma_grooves_path): + self.__config.set_mma_grooves_path(new_mma_grooves_path) + self.__grooves.load_grooves(False) + # chord sheet font + new_chord_sheet_font = self.__fontbutton1.get_font_name() + if (self.__config.get_chord_sheet_font() != new_chord_sheet_font): + self.__config.set_chord_sheet_font(new_chord_sheet_font) + self.__gui.refresh_chord_sheet() + # connect to JACK on startup + self.__config.set_jack_connect_startup(self.__checkbutton2.get_active()) diff --git a/src/main/python/linuxband/gui/save_button_status.py b/src/main/python/linuxband/gui/save_button_status.py new file mode 100644 index 0000000..51fbadc --- /dev/null +++ b/src/main/python/linuxband/gui/save_button_status.py @@ -0,0 +1,35 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gobject + + +class SaveButtonStatus(object): + + def __init__(self, glade, song): + button = glade.get_widget("toolbutton9") + menuitem = glade.get_widget("imagemenuitem3") + mainwindow = glade.get_widget("mainWindow") + self.__sourceId = gobject.timeout_add(800, self.__refresh_save_button_status, song, button, menuitem, mainwindow) + + def __refresh_save_button_status(self, song, button, menuitem, mainwindow): + save_needed = song.get_data().is_save_needed() + button.set_sensitive(save_needed) + menuitem.set_sensitive(save_needed) + prefix = "*" if save_needed else "" + mainwindow.set_title(prefix + song.get_data().get_title() + " | Linux Band") + return True diff --git a/src/main/python/linuxband/gui/source_editor.py b/src/main/python/linuxband/gui/source_editor.py new file mode 100644 index 0000000..ff41f67 --- /dev/null +++ b/src/main/python/linuxband/gui/source_editor.py @@ -0,0 +1,175 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gobject +import gtk +import gtksourceview2 +import pango +import logging +from linuxband.glob import Glob + + +class SourceEditor(object): + + def __init__(self, glade, song): + self.__song = song + self.__init_gui(glade) + self.__playhead_pos = -1 + self.__error_mark_pos = -1 + + def move_playhead_to(self, pos): + """ Highlight the line just being played. """ + logging.debug("MOVING PLAYHEAD TO %i" % pos) + buff = self.__sourcebuffer + # remove the black background and the playhead marker + start, end = buff.get_bounds() + buff.remove_tag_by_name("playhead", start, end) + buff.remove_source_marks(start, end, self.LINE_MARKER) + # draw the background and the playhead marker again + if pos > -1: + iter1 = buff.get_iter_at_line(pos) + iter2 = buff.get_iter_at_line(pos + 1) + buff.apply_tag_by_name("playhead", iter1, iter2) + buff.create_source_mark(None, self.LINE_MARKER, iter1) + self.__playhead_pos = pos + + def put_error_mark_to(self, line): + """ Put the red error mark at the beginning of the given line. """ + buff = self.__sourcebuffer + start, end = buff.get_bounds() + # remove the red background and the error mark + buff.remove_tag_by_name("error", start, end) + buff.remove_source_marks(start, end, self.ERROR_MARKER) + # draw the red background and the error mark again + if line > -1: + # text background redimport logging + iter1 = buff.get_iter_at_line(line) + iter2 = buff.get_iter_at_line(line + 1) + buff.apply_tag_by_name("error", iter1, iter2) + # error marker + it = buff.get_iter_at_line(line) + buff.create_source_mark(None, self.ERROR_MARKER, it) + self.__error_mark_pos = line + + def new_song_loaded(self, mma_data): + self.refresh_source(mma_data) + self.put_error_mark_to(-1) + self.__move_cursor_home() + + def refresh_source(self, mma_data): + """ Loads the new text into the source view, preserves cursor position, preserves marks. """ + buff = self.__sourcebuffer + # load new text, preserve cursor position + offset = buff.props.cursor_position + buff.set_text(mma_data) + it = buff.get_iter_at_offset(offset) + buff.place_cursor(it) + # refresh marks + self.put_error_mark_to(self.__error_mark_pos) + self.move_playhead_to(self.__playhead_pos) + + def grab_focus(self): + gobject.idle_add(self.__gtksourceview.grab_focus) + + def has_focus(self): + return self.__gtksourceview.is_focus() + + def cut_selection(self): + buff = self.__sourcebuffer + buff.cut_clipboard(gtk.clipboard_get(), True) + + def copy_selection(self): + buff = self.__sourcebuffer + buff.copy_clipboard(gtk.clipboard_get()) + + def paste_selection(self): + buff = self.__sourcebuffer + buff.paste_clipboard(gtk.clipboard_get(), None, True) + + def delete_selection(self): + buff = self.__sourcebuffer + buff.delete_selection(False, True) + + def select_all(self): + buff = self.__sourcebuffer + buff.select_range(buff.get_start_iter(), buff.get_end_iter()) + + def __init_gui(self, glade): + scrolledwindow6 = glade.get_widget("scrolledwindow6") + + self.__gtksourceview = gtksourceview2.View() + self.__sourcebuffer = gtksourceview2.Buffer() + + view = self.__gtksourceview + buff = self.__sourcebuffer + + view.set_buffer(buff) + scrolledwindow6.add(view) + + view.set_auto_indent(True) + view.set_highlight_current_line(True) + view.set_show_line_numbers(True) + view.set_show_line_marks(True) + view.set_show_right_margin(True) + view.set_insert_spaces_instead_of_tabs(True) + view.set_tab_width(4) + view.set_indent_width(-1) + view.set_right_margin_position(80) + view.set_smart_home_end(True) + view.set_indent_on_tab(True) + view.set_left_margin(2) + view.set_right_margin(2) + view.modify_font(pango.FontDescription("mono 10")) + view.set_wrap_mode(gtk.WRAP_NONE) + # playhead text attributes + tag = buff.create_tag("playhead") + tag.props.background = "black" + tag.props.background_set = True + tag.props.foreground = "white" + tag.props.foreground_set = True + # error text attributes + tag = buff.create_tag("error") + tag.props.background = "red" + tag.props.background_set = True + # playhead marker + self.LINE_MARKER = 'lineMarker' + pixbuf = gtk.gdk.pixbuf_new_from_file(Glob.LINE_MARKER) + view.set_mark_category_pixbuf(self.LINE_MARKER, pixbuf) + # error marker + self.ERROR_MARKER = 'errorMarker' + pixbuf = gtk.gdk.pixbuf_new_from_file(Glob.ERROR_MARKER) + view.set_mark_category_pixbuf(self.ERROR_MARKER, pixbuf) + # buff parameters + buff.set_highlight_syntax(True) + buff.set_highlight_matching_brackets(True) + buff.set_max_undo_levels(50) + # when buff modified save needed + buff.connect("modified-changed", self.__modified_changed) + # and show it + self.__gtksourceview.show() + + def __modified_changed(self, buff): + if self.__gtksourceview.is_focus(): + buff = self.__sourcebuffer + mma_data = buff.get_text(buff.get_start_iter(), buff.get_end_iter()) + self.__song.load_from_string(mma_data) + buff.set_modified(False) + + def __move_cursor_home(self): + buff = self.__sourcebuffer + it = buff.get_iter_at_offset(0) + buff.place_cursor(it) diff --git a/src/main/python/linuxband/logger.py b/src/main/python/linuxband/logger.py new file mode 100644 index 0000000..2cbc688 --- /dev/null +++ b/src/main/python/linuxband/logger.py @@ -0,0 +1,27 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import sys + +class Logger(object): + + @staticmethod + def initLogging(log_level): + consoleFormat = "%(asctime)s %(levelname)s %(funcName)s %(message)s" + dateFormat = "%H:%M:%S" + logging.basicConfig(stream=sys.stdout, level=log_level, format=consoleFormat, datefmt=dateFormat) diff --git a/src/main/python/linuxband/midi/__init__.py b/src/main/python/linuxband/midi/__init__.py new file mode 100644 index 0000000..c3245bf --- /dev/null +++ b/src/main/python/linuxband/midi/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/src/main/python/linuxband/midi/midi_player.py b/src/main/python/linuxband/midi/midi_player.py new file mode 100644 index 0000000..4ae5a22 --- /dev/null +++ b/src/main/python/linuxband/midi/midi_player.py @@ -0,0 +1,229 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import threading +import os +import fcntl +import select +import subprocess +import gobject +import logging +from linuxband.glob import Glob + +class MidiPlayer: + + __COMM_LOAD = "LOAD" + __COMM_PLAY = "PLAY" + __COMM_PLAY_BAR = "PLAY_BAR" + __COMM_PLAY_BARS = "PLAY_BARS" + __COMM_STOP = "STOP" + __COMM_PAUSE_ON = "PAUSE_ON" + __COMM_PAUSE_OFF = "PAUSE_OFF" + __COMM_LOOP_ON = "LOOP_ON" + __COMM_LOOP_OFF = "LOOP_OFF" + __COMM_JACK_TRANSPORT_ON = "TRANSPORT_ON" + __COMM_JACK_TRANSPORT_OFF = "TRANSPORT_OFF" + __COMM_INTRO_LENGTH = "INTRO_LENGTH" + __COMM_FINISH = "FINISH" + + __EVENT_BAR_NUM = "BAR_NUMBER" + __EVENT_LINE_NUM = "LINE_NUMBER" + __EVENT_SONG_END = "SONG_END" + + __SEPARATOR = ' ' + + def __init__(self, gui): + self.__gui = gui + self.__mma_line_offset = 0 + self.__player = None + self.__receive_thread = None + self.__piper = None + self.__pipew = None + self.__pout = None + + self.__saved_intro_length = 2 + self.__saved_pause = False + self.__saved_loop = True + self.__saved_transport = True + self.__saved_midi_data = None + self.__saved_offset = None + + self.__playing = False + self.__connected = False + + def startup(self): + piper, pipew = os.pipe() + self.__piper = piper + self.__pipew = pipew + + pipeName = '/proc/' + str(Glob.PID) + '/fd/' + str(pipew) + command = [ Glob.PLAYER_PROGRAM, '-s', '-n', '-x', pipeName ] + if Glob.CONSOLE_LOG_LEVEL == logging.DEBUG: + command.insert(1, '-d') + try: + self.__player = subprocess.Popen(command, stdin=subprocess.PIPE) + except: + logging.exception("Failed to run command '%s'", ' '.join(command)) + os.close(piper) + os.close(pipew) + return + # sending data to midi player should not block + out = self.__player.stdin.fileno() + flags = fcntl.fcntl(out, fcntl.F_GETFL) + fcntl.fcntl(out, fcntl.F_SETFL, flags | os.O_NONBLOCK) + self.__pout = out + # start receive thread listening for incoming events + self.__receive_thread = threading.Thread(target=self.__receive_data) + self.__receive_thread.start() + self.__connected = True + self.__resend_data() + + def shutdown(self): + if self.__receive_thread: + os.write(self.__pipew, MidiPlayer.__COMM_FINISH + MidiPlayer.__SEPARATOR) + self.__receive_thread.join() + self.__receive_thread = None + if self.__player: + self.playback_stop() + self.__send_token(MidiPlayer.__COMM_FINISH) + self.__player.wait() + self.__player = None + # descriptors could have been already closed + try: + os.close(self.__piper) + except: + pass + try: + os.close(self.__pipew) + except: + pass + try: + os.close(self.__pout) + except: + pass + + def load_smf_data(self, midi_data, offset): + self.__mma_line_offset = offset + self.__send_token(MidiPlayer.__COMM_LOAD) + self.__send_token(str(len(midi_data))) + self.__send_data(midi_data) + self.__saved_midi_data = midi_data + self.__saved_offset = offset + + def playback_start(self): + self.__send_token(MidiPlayer.__COMM_PLAY) + self.__saved_pause = False + + def playback_start_bar(self, bar): + self.__send_token(MidiPlayer.__COMM_PLAY_BAR) + self.__send_token(str(bar)) + self.__saved_pause = False + + def playback_start_bars(self, bars): + self.__send_token(MidiPlayer.__COMM_PLAY_BARS) + self.__send_token(str(bars[0])) + self.__send_token(str(bars[1])) + self.__saved_pause = False + + def playback_stop(self): + self.__send_token(MidiPlayer.__COMM_STOP) + self.__saved_pause = False + + def set_pause(self, pause): + comm = MidiPlayer.__COMM_PAUSE_ON if pause else MidiPlayer.__COMM_PAUSE_OFF + self.__send_token(comm) + self.__saved_pause = pause + + def set_loop(self, loop): + comm = MidiPlayer.__COMM_LOOP_ON if loop else MidiPlayer.__COMM_LOOP_OFF + self.__send_token(comm) + self.__saved_loop = loop + + def use_jack_transport(self, transport): + comm = MidiPlayer.__COMM_JACK_TRANSPORT_ON if transport else MidiPlayer.__COMM_JACK_TRANSPORT_OFF + self.__send_token(comm) + self.__saved_transport = transport + + def set_intro_length(self, length): + self.__send_token(MidiPlayer.__COMM_INTRO_LENGTH) + self.__send_token(str(length)) + self.__saved_intro_length = length + + def is_playing(self): + return self.__playing + + def __resend_data(self): + if self.__saved_midi_data != None: + self.load_smf_data(self.__saved_midi_data, self.__saved_offset) + self.set_intro_length(self.__saved_intro_length) + self.set_pause(self.__saved_pause) + self.set_loop(self.__saved_loop) + self.use_jack_transport(self.__saved_transport) + + def __read_token(self, pipe): + token = [] + ch = os.read(pipe, 1) + while ch != MidiPlayer.__SEPARATOR: + token.append(ch) + ch = os.read(pipe, 1) + token = ''.join(token) + logging.debug(token) + return ''.join(token) + + def __receive_data(self): + while True: + logging.debug("Waiting for events") + token = self.__read_token(self.__piper) + if token == MidiPlayer.__EVENT_BAR_NUM: + # move the playhead to the new position + bar_num = int(self.__read_token(self.__piper)) + gobject.idle_add(self.__gui.move_playhead_to_bar, bar_num) + self.__playing = True + elif token == MidiPlayer.__EVENT_LINE_NUM: + # move the playhead2 to the new position + lineNum = int(self.__read_token(self.__piper)) + gobject.idle_add(self.__gui.move_playhead_to_line, lineNum - self.__mma_line_offset - 1) + self.__playing = True + elif token == MidiPlayer.__EVENT_SONG_END: + # playback has finished + gobject.idle_add(self.__gui.hide_playhead) + self.__playing = False + elif token == MidiPlayer.__COMM_FINISH: + logging.debug("Thread finishing") + return + else: + logging.debug("Unrecognized data '%s'", token) + + def __send_token(self, comm): + logging.debug(comm) + self.__send_data(comm) + self.__send_data(MidiPlayer.__SEPARATOR) + + def __send_data(self, data): + if self.__connected: + logging.debug("Sending %i bytes", len(data)) + try: + timeout = 2 + if not select.select([], [self.__pout], [], timeout)[1]: + logging.error("Cannot send data to midi player. Timeout after " \ + + str(timeout) + " seconds.") + else: + os.write(self.__pout, data) + except: + logging.error("Failed to send data to midi player. Ensure the JACK server is running and hit the JACK reconnect button.") + else: + logging.debug("Not yet connected.") diff --git a/src/main/python/linuxband/midi/mma2smf.py b/src/main/python/linuxband/midi/mma2smf.py new file mode 100644 index 0000000..1242e83 --- /dev/null +++ b/src/main/python/linuxband/midi/mma2smf.py @@ -0,0 +1,130 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import select +import subprocess +import string +from linuxband.glob import Glob +import logging + + +class MidiGenerator(object): + + def __init__(self, config): + self.__config = config + + def check_mma_syntax(self, mma_data): + """ + < -1 other error, = -1 MMA error unknown line, 0 is OK, > 0 MMA error line + """ + mmainput = '/proc/self/fd/0' + command = [ self.__config.get_mma_path(), mmainput, '-n' ] # -n No generation of midi output + try: + mma = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + except: + logging.exception("Failed to run command '" + ' '.join(command) + "'") + return -2 + try: + fout = mma.stdin + fout.write(mma_data) + fout.close() + except: + logging.exception("Failed to send data to MMA") + return -2 + exit_status = mma.wait() + if (exit_status != 0): + logging.error("Failed generating midi data. MMA returned status code " + str(exit_status)) + outstr = mma.stdout.readlines() + num = self.__parse_error_line_number(outstr) + logging.error(''.join(outstr)) + return num + return 0 + + def generate_smf(self, mma_data): + """ + Convert mma_data string into midi_data string using MMA program. + """ + piper, pipew = os.pipe() + try: + res_and_midi = self.__do_generate_smf(piper, pipew, mma_data) + finally: + try: + os.close(piper) + except: + pass + try: + os.close(pipew) + except: + pass + return res_and_midi + + def __do_generate_smf(self, piper, pipew, mma_data): + """ + < -1 other error, = -1 MMA error unknown line, 0 is OK, > 0 MMA error line + """ + mmainput = '/proc/self/fd/0' + mmaoutput = '/proc/' + str(Glob.PID) + '/fd/' + str(pipew) + command = [ self.__config.get_mma_path(), mmainput, '-f' , mmaoutput ] + try: + mma = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + except: + logging.exception("Failed to run command '" + ' '.join(command) + "'") + return (-2, '') + try: + fout = mma.stdin + fout.write(mma_data) + fout.close() + except: + logging.exception("Failed to send data to MMA") + return (-2, '') + try: + fin = os.fdopen(piper, 'r') + timeout = 2 + if not select.select([fin], [], [], timeout)[0]: + logging.error("MMA generated no output. Timeout after " + str(timeout) + " seconds.") + outstr = mma.stdout.readlines() + logging.error(' '.join(outstr)) + return (-1, '') + os.close(pipew) # close our write pipe end, now only MMA has it opened + midi_data = fin.read() + except: + logging.exception("Failed to read midi data from MMA") + exit_status = mma.wait() + if (exit_status != 0): + logging.error("Failed generating midi data. MMA returned status code " + str(exit_status)) + return (-2, '') + return (0, midi_data) + + def __parse_error_line_number(self, lines): + """ + Parse out the error line number. Error line example: ERROR: + """ + for i, line in enumerate(lines): #@UnusedVariable + res = string.find(line, "ERROR") + if res != -1: break + if res == -1: return -1 + start = string.find(line, "<") + end = string.find(line, ">") + if start == -1 or end == -1: return -1 + lmidd = line[start + 1: end] + midds = lmidd.split() + if len(midds) != 2: return -1 + try: + return int(midds[1]) + except TypeError: + return -1 diff --git a/src/main/python/linuxband/mma/__init__.py b/src/main/python/linuxband/mma/__init__.py new file mode 100644 index 0000000..c3245bf --- /dev/null +++ b/src/main/python/linuxband/mma/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/src/main/python/linuxband/mma/bar_chords.py b/src/main/python/linuxband/mma/bar_chords.py new file mode 100644 index 0000000..c321c11 --- /dev/null +++ b/src/main/python/linuxband/mma/bar_chords.py @@ -0,0 +1,132 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import copy +import logging + + +class BarChords: + + def __init__(self): + self.__song_data = None + self.__before_number = '' + self.__number = None + self.__after_number = ' ' + self.__chords = [[ '/', '' ]] # one chord is always there + self.__eol = '\n' + + def set_song_data(self, song_data): + self.__song_data = song_data + + def set_before_number(self, before_number): + self.__before_number = before_number + + def get_before_number(self): + return self.__before_number + + def get_number(self): + return self.__number + + def set_number(self, number): + self.__number = number + + def set_after_number(self, after_number): + self.__after_number = after_number + + def get_after_number(self): + return self.__after_number + + def set_chords(self, chords): + self.__chords = chords + + def get_chords(self): + return self.__chords + + def set_eol(self, eol): + self.__eol = eol + + def get_eol(self): + return self.__eol + + def set_chord(self, beat_num, chord): + """ + Save one chord on given beat. + + If chord == '' then actually delete the chord. + """ + # [['Dm', ' '], ['/', ' '], ['AmzC@3.2', ' '], ['z!', '\n']] + chords = self.__chords + song_data = self.__song_data + if chord == '' and beat_num >= len(self.__chords): + return + if beat_num + 1 < len(chords): # there's a chord after this beat + full_chord = chords[beat_num] + if not chord: chord = '/' + if full_chord[0] != chord: + full_chord[0] = chord + song_data.changed() + else: + if not chord: # delete a chord + full_chord = chords[beat_num] + if beat_num > 0: # there is some chord before us + # possibly move trailing string from our chord to eol + self.__eol = ''.join(full_chord[1:]) + self.__eol + chords.pop(beat_num) + song_data.changed() + else: # the only chord on the line + if full_chord[0] != '/': + full_chord[0] = '/' + song_data.changed() + else: # add or replace chord + if beat_num < len(chords): # replace an existing chord + full_chord = chords[beat_num] + if full_chord[0] != chord: + full_chord[0] = chord + song_data.changed() + else: # append a chord + last_full_chord = chords[len(chords) - 1] + if len(last_full_chord[1]) == len(last_full_chord[1].rstrip()): + last_full_chord[1] = last_full_chord[1] + ' ' + while len(chords) < beat_num: chords.append(['/', ' ']) + chords.append([chord, '']) + song_data.changed() + + def get_as_string_list(self): + res = [] + res.append(self.__before_number) + if self.get_number() != None: + res.append(str(self.get_number())) + res.append(self.__after_number) + for full_chord in self.__chords: + res.extend(full_chord) + res.append(self.__eol) + return res + + def show_debug(self): + logging.debug("Num: '%s'" % self.__number) + logging.debug("AfterNum: '%s'" % self.__after_number) + logging.debug("Chords: '%s'" % self.__chords) + logging.debug("Eol: '%s'" % self.__eol) + + def __deepcopy__(self, memo): + newone = BarChords() + newone.__song_data = self.__song_data + newone.__number = self.__number + newone.__after_number = copy.deepcopy(self.__after_number, memo) + newone.__chords = copy.deepcopy(self.__chords, memo) + newone.__eol = copy.deepcopy(self.__eol, memo) + return newone diff --git a/src/main/python/linuxband/mma/bar_info.py b/src/main/python/linuxband/mma/bar_info.py new file mode 100644 index 0000000..c7dbf20 --- /dev/null +++ b/src/main/python/linuxband/mma/bar_info.py @@ -0,0 +1,206 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import copy +import logging +from linuxband.glob import Glob + + +class BarInfo: + + def __init__(self): + self.__song_data = None + self.__lines = [] + self.__events = [] + + def set_song_data(self, song_data): + self.__song_data = song_data + + def add_line(self, line): + self.__lines.append(line) + if line[0] in Glob.EVENTS: + self.__events.append(line) + + def insert_line(self, line): + """ The same as add_line but inserting at the beginning """ + self.__lines.insert(0, line) + if line[0] in Glob.EVENTS: + self.__events.insert(0, line) + + def get_events(self): + return self.__events + + # methods used by ChordSheet + def has_events(self): + return True if len(self.__events) > 0 else False + + def has_repeat_begin(self): + return self.__lookup_action(Glob.A_REPEAT) != None + + def has_repeat_end(self): + return self.__lookup_action(Glob.A_REPEAT_ENDING) or self.__lookup_action(Glob.A_REPEAT_END) + + def show_debug(self): + for i, v in enumerate(self.__lines): + logging.debug("%i %s ", i, v) + + def get_groove(self): + return self.__lookup_action(Glob.A_GROOVE) + + def get_tempo(self): + return self.__lookup_action(Glob.A_TEMPO) + + def get_lines(self): + return self.__lines + + def add_event(self, line): + if line[0] in [ Glob.A_REPEAT_END, Glob.A_REPEAT_ENDING ]: + self.insert_line(line) + else: + self.add_line(line) + self.__song_data.changed() + + def remove_event(self, line): + self.__lines.remove(line) + self.__events.remove(line) + self.__song_data.changed() + + def replace_event(self, old, new): + index = self.__find_element(self.__lines, old) + self.__lines[index] = new + index = self.__find_element(self.__events, old) + self.__events[index] = new + self.__song_data.changed() + + def move_event_backwards(self, line): + index = self.__events.index(line) + if index > 0: + previous = self.__events[index - 1] + self.__swap_events(line, previous, self.__lines) + self.__swap_events(line, previous, self.__events) + self.__song_data.changed() + + def move_event_forwards(self, line): + index = self.__events.index(line) + if index < len(self.__events) - 1: + next_event = self.__events[index + 1] + self.__swap_events(line, next_event, self.__lines) + self.__swap_events(line, next_event, self.__events) + self.__song_data.changed() + + def get_as_string_list(self): + res = [] + for line in self.__lines: + if line[0] == Glob.A_BEGIN_BLOCK: + res.extend(line[2:]) + else: + res.extend(line[1:]) + return res + + def __lookup_action(self, action): + for item in self.__lines: + if item[0] == action: + return item + return None + + def __find_element(self, lst, elem): + for index in range(0, len(lst)): + if lst[index] is elem: + return index + + def __swap_events(self, event1, event2, lst): + index1 = lst.index(event1) + index2 = lst.index(event2) + lst[index1] = event2 + lst[index2] = event1 + + def __deepcopy__(self, memo): + newone = BarInfo() + newone.__song_data = self.__song_data + newone.__lines = copy.deepcopy(self.__lines, memo) + newone.__events = copy.deepcopy(self.__events, memo) + return newone + + @staticmethod + def create_event(eventTitle): + eventsInit = { Glob.A_GROOVE: [ Glob.A_GROOVE, "Groove", " ", "50sRock", "\n" ], + Glob.A_TEMPO: [ Glob.A_TEMPO, "Tempo", " ", "120", "\n" ], + Glob.A_REPEAT: [ Glob.A_REPEAT, "Repeat", "\n" ], + Glob.A_REPEAT_ENDING: [ Glob.A_REPEAT_ENDING, "RepeatEnding", "\n" ], + Glob.A_REPEAT_END: [ Glob.A_REPEAT_END, "RepeatEnd", "\n" ] } + return copy.deepcopy(eventsInit[eventTitle]) + + @staticmethod + def set_groove_value(line, groove): + line[3] = groove + + @staticmethod + def get_groove_value(line): + return line[3] + + @staticmethod + def set_tempo_value(line, tempo): + line[3] = tempo + + @staticmethod + def get_tempo_value(line): + return line[3] + + @staticmethod + def set_repeat_end_value(line, count): + # example line: ['REPEATEND', 'RepeatEnd', ' ', '2', '\n'] + if len(line) > 3: # there is already some number + if count == 2: + line.pop(2) + line.pop(2) + else: + line[3] = count + else: # no number yet, example: ['REPEATEND', 'RepeatEnd', '\n'] + if count != 2: + line.insert(2, count) + line.insert(2, ' ') + + @staticmethod + def get_repeat_end_value(line): + return line[3] if len(line) > 3 else "2" + + @staticmethod + def set_repeat_ending_value(line, count): + BarInfo.set_repeat_end_value(line, count) + + @staticmethod + def get_repeat_ending_value(line): + return BarInfo.get_repeat_end_value(line) + + @staticmethod + def get_doc_value(line): + res = line[3] + res = res.split() + res = ' '.join(res) + return res + + @staticmethod + def get_author_value(line): + return line[2].strip() + + @staticmethod + def get_time_value(line): + return line[2] + + @staticmethod + def get_defgroove_value(line): + return line[3], ' '.join(line[4].replace('\\\n', '').split()) diff --git a/src/main/python/linuxband/mma/chord_table.py b/src/main/python/linuxband/mma/chord_table.py new file mode 100644 index 0000000..476f3ac --- /dev/null +++ b/src/main/python/linuxband/mma/chord_table.py @@ -0,0 +1,458 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This file includes code found in MMA's 12.01 chordtable.py file, +written by Bob van der Poel + +Table of chords. All are based on a C scale. +Generating chords is easy in MIDI since we just need to +add/subtract constants, based on yet another table. + +CAUTION, if you add to this table make sure there are at least +3 notes in each chord! Don't make any chord longer than 8 notes +(The pattern define sets volumes for only 8). + +There is a corresponding scale set for each chord. These are +used by bass and scale patterns. + +Each chord needs an English doc string. This is extracted by the +-Dn option to print a table of chordnames for inclusion in the +reference manual. + +""" + +C = 0 +Cs = Db = 1 +D = 2 +Ds = Eb = 3 +E = Fb = 4 +Es = F = 5 +Fs = Gb = 6 +G = 7 +Gs = Ab = 8 +A = Bbb= 9 +As = Bb = 10 +B = Cb = 11 + +chordlist = { + 'M': ((C, E, G ), + (C, D, E, F, G, A, B), + "Major triad. This is the default and is used in " + "the absense of any other chord type specification."), + + '(b5)': ((C, E, Gb ), + (C, D, E, F, Gb, A, B), + "Major triad with flat 5th."), + + 'add9': ((C, E, G, D+12), + (C, D, E, F, G, A, D+12), + "Major chord plus 9th (no 7th.)"), + + 'm': ((C, Eb, G ), + (C, D, Eb, F, G, Ab, Bb), + "Minor triad."), + + 'mb5': ((C, Eb, Gb ), + (C, D, Eb, F, Gb, Ab, Bb), + "Minor triad with flat 5th (aka dim)."), + + 'm#5': ((C, Eb, Gs ), + (C, D, Eb, F, Gs, Ab, Bb), + "Minor triad with augmented 5th."), + + 'm6': ((C, Eb, G, A ), + (C, D, Eb, F, G, A, Bb), + "Minor 6th (flat 3rd plus a 6th)."), + + 'm6(add9)': ((C, Eb, G, D+12, A+12), + (C, D, Eb, F, G, A, Bb), + "Minor 6th with added 9th. This is sometimes notated as a slash chord " + "in the form ``m6/9''." ), + + 'm7': ((C, Eb, G, Bb ), + (C, D, Eb, F, G, Ab, Bb), + "Minor 7th (flat 3rd plus dominant 7th)."), + + 'mM7': ((C, Eb, G, B ), + (C, D, Eb, F, G, Ab, B), + "Minor Triad plus Major 7th. You will also see this printed " + "as ``m(maj7)'', ``m+7'', ``min(maj7)'' and ``min$\sharp$7'' " + "(which \mma\ accepts); as well as the \mma\ \emph{invalid} " + "forms: ``-($\Delta$7)'', and ``min$\\natural$7''."), + + 'm+7b9': ((C, Eb, Gs, Bb, Db+12), + (C, Db, Eb, F, Gs, Ab, Bb), + "Augmented minor 7 plus flat 9th."), + + 'm+7#9': ((C, Eb, Gs, Bb, Ds+12), + (C, Ds, Eb, F, Gs, Ab, Bb), + "Augmented minor 7 plus sharp 9th."), + + 'mM7(add9)': ((C, Eb, G, B, D+12), + (C, D, Eb, F, G, Ab, B), + "Minor Triad plus Major 7th and 9th."), + + 'm7b5': ((C, Eb, Gb, Bb ), + (C, D, Eb, F, Gb, Ab, Bb), + "Minor 7th, flat 5 (aka 1/2 diminished). "), + + 'm7b9': ((C, Eb, G, Bb, Db+12 ), + (C, Db, Eb, F, G, Ab, Bb), + "Minor 7th with added flat 9th."), + + 'm7#9': ((C, Eb, G, Bb, Ds+12 ), + (C, Ds, Eb, F, G, Ab, Bb), + "Minor 7th with added sharp 9th."), + + '7': ((C, E, G, Bb ), + (C, D, E, F, G, A, Bb), + "7th."), + + '7b5': ((C, E, Gb, Bb ), + (C, D, E, F, Gb, A, Bb), + "7th, flat 5."), + + 'dim7': ((C, Eb, Gb, Bbb ), + (C, D, Eb, F, Gb, Ab, Bbb ), # missing 8th note + "Diminished seventh."), + + 'dim7(addM7)': ((C, Eb, Gb, A, B), + (C, D, Eb, F, Gb, A, B), + "Diminished tirad with added Major 7th."), + + 'aug': ((C, E, Gs ), + (C, D, E, F, Gs, A, B ), + "Augmented triad."), + + '6': ((C, E, G, A ), + (C, D, E, F, G, A, B), + "Major tiad with added 6th."), + + '6(add9)': ((C, E, G, D+12, A+12), + (C, D, E, F, G, A, B), + "6th with added 9th. This is sometimes notated as a slash chord " + "in the form ``6/9''."), + + 'M7': ((C, E, G, B), + (C, D, E, F, G, A, B), + "Major 7th."), + + 'M7#5': ((C, E, Gs, B), + (C, D, E, F, Gs, A, B), + "Major 7th with sharp 5th."), + + 'M7b5': ((C, E, Gb, B ), + (C, D, E, F, Gb, A, B ), + "Major 7th with a flat 5th."), + + '9': ((C, E, G, Bb, D+12 ), + (C, D, E, F, G, A, Bb), + "7th plus 9th."), + + + + '9b5': ((C, E, Gb, Bb, D+12 ), + (C, D, E, F, Gb, A, Bb), + "7th plus 9th with flat 5th."), + + 'm9': ((C, Eb, G, Bb, D+12 ), + (C, D, Eb, F, G, Ab, Bb), + "Minor triad plus 7th and 9th."), + + 'm7b5b9': ((C, Eb, Gb, Bb, Db+12), + (C, Db, Eb, F, Gb, Ab, Bb), + "Minor 7th with flat 5th and flat 9th."), + + 'm9b5': ((C, Eb, Gb, Bb, D+12 ), + (C, D, Eb, F, Gb, Ab, Bb), + "Minor triad, flat 5, plus 7th and 9th."), + + 'm(sus9)':((C, Eb, G, D+12 ), + (C, D, Eb, F, G, Ab, D+12), + "Minor triad plus 9th (no 7th)."), + + 'M9': ((C, E, G, B, D+12 ), + (C, D, E, F, G, A, B), + "Major 7th plus 9th."), + + 'M9#11': ((C, E, G, B, D+12, Fs+12), + (C, D, E, Fs, G, A, B), + "Major 9th plus sharp 11th."), + + '7b9': ((C, E, G, Bb, Db+12 ), + (C, Db, E, F, G, A, Bb), + "7th with flat 9th."), + + '7#9': ((C, E, G, Bb, Ds+12 ), + (C, Ds, E, F, G, A, Bb), + "7th with sharp 9th."), + + '7#9b13': ((C, E, G, Bb, Ds+12, Ab+12 ), + (C, Ds, E, F, G, Ab, Bb), + "7th with sharp 9th and flat 13th."), + + '7b5b9':((C, E, Gb, Bb, Db+12 ), + (C, Db, E, F, Gb, A, Bb), + "7th with flat 5th and flat 9th."), + + '7b5#9':((C, E, Gb, Bb, Ds+12 ), + (C, Ds, E, F, Gb, A, Bb), + "7th with flat 5th and sharp 9th."), + + '7#5#9':((C, E, Gs, Bb, Ds+12 ), + (C, Ds, E, F, Gs, A, Bb), + "7th with sharp 5th and sharp 9th."), + + + 'aug7': ((C, E, Gs, Bb ), + (C, D, E, F, Gs, A, Bb), + "An augmented chord (raised 5th) with a dominant 7th."), + + 'aug7b9':((C, E, Gs, Bb, Db+12 ), + (C, Db, E, F, Gs, A, Bb), + "An augmented chord (raised 5th) with a dominant 7th and flat 9th."), + + 'aug7#9':((C, E, Gs, Bb, Ds+12 ), + (C, Ds, E, F, Gs, A, Bb), + "An augmented chord (raised 5th) with a dominant 7th and sharp 9th."), + + 'aug9M7':((C, E, Gs, B, D+12 ), + (C, D, E, F, Gs, A, B), + "An augmented chord (raised 5th) with a major 7th and 9th."), + + '+7b9#11': ((C, E, Gs, Bb, Db+12, Fs+12), + (C, Db, E, Fs, G, A, Bb), + "Augmented 7th with flat 9th and sharp 11th."), + + 'm+7b9#11': ((C, Eb, Gs, Bb, Db+12, Fs+12), + (C, Db, Eb, Fs, Gs, A, Bb), + "Augmented minor 7th with flat 9th and sharp 11th."), + + '11': ((C, C, G, Bb, D+12, F+12 ), + (C, D, E, F, G, A, Bb), + "9th chord plus 11th (3rd not voiced)."), + + 'm11': ((C, Eb, G, Bb, D+12, F+12 ), + (C, D, Eb, F, G, Ab, Bb), + "9th with minor 3rd, plus 11th."), + + 'm7(add11)': ((C, Eb, G, Bb, F+12 ), + (C, D, Eb, F, G, Ab, Bb), + "Minor 7th plus 11th."), + + 'm9#11': ((C, Eb, G, Bb, D+12, Fs+12), + (C, D, Eb, Fs, G, A, Bb), + "Minor 7th plus 9th and sharp 11th."), + + 'm7b9#11': ((C, Eb, G, Bb, Db+12, Fs+12), + (C, Db, Eb, Fs, G, A, Bb), + "Minor 7th plus flat 9th and sharp 11th."), + + 'm7(add13)': ((C, Eb, G, Bb, A+12 ), + (C, D, Eb, F, G, A, Bb), + "Minor 7th plus 13th."), + + '11b9': ((C, E, G, Bb, Db+12, F+12 ), + (C, Db, E, F, G, A, Bb), + "7th chord plus flat 9th and 11th."), + + '9#5': ((C, E, Gs, Bb, D+12 ), + (C, D, E, F, Gs, A, Bb), + "7th plus 9th with sharp 5th (same as aug9)."), + + '9#11': ((C, E, G, Bb, D+12, Fs+12 ), + (C, D, E, Fs, G, A, Bb), + "7th plus 9th and sharp 11th."), + + '7#9#11':((C, E, G, Bb, Ds+12, Fs+12 ), + (C, Ds, E, Fs, G, A, Bb), + "7th plus sharp 9th and sharp 11th."), + + '7b9#11': ((C, E, G, Bb, Db+12, Fs+12 ), + (C, Db, E, Fs, G, A, Bb), + "7th plus flat 9th and sharp 11th."), + + '7#11':((C, E, G, Bb, Fs+12 ), + (C, D, E, Fs, G, A, Bb), + "7th plus sharp 11th (9th omitted)."), + + 'M7#11':((C, E, G, B, Fs+12 ), + (C, D, E, Fs, G, A, B), + "Major 7th plus sharp 11th (9th omitted)."), + + 'm11b5': ((C, Eb, Gb, Bb, D+12, F+12), + (C, D, Eb, F, Gb, A, Bb), + "Minor 7th with flat 5th plus 11th."), + + # Sus chords. Not sure what to do with the associated scales. For + # now just duplicating the 2nd or 3rd in the scale seems to make sense. + + 'sus4': ((C, F, G ), + (C, D, F, F, G, A, B), + "Suspended 4th, major triad with the 3rd raised half tone."), + + '7sus': ((C, F, G, Bb ), + (C, D, F, F, G, A, Bb), + "7th with suspended 4th, dominant 7th with 3rd " + "raised half tone."), + + '7susb9': ((C, F, G, Bb, Db+12), + (C, Db, F, F, G, A, Bb), + "7th with suspended 4th and flat 9th."), + + 'sus2': ((C, D, G ), + (C, D, D, F, G, A, B), + "Suspended 2nd, major triad with the major 2nd above the " + "root substituted for 3rd."), + + '7sus2':((C, D, G, Bb ), + (C, D, D, F, G, A, Bb), + "A sus2 with dominant 7th added."), + + 'sus9': ((C, F, G, Bb, D+12), + (C, D, F, F, G, A, Bb), + "7sus plus 9th."), + + '13sus': ((C, F, G, Bb, D+12, A+12), + (C, D, F, F, G, A, Bb), + "7sus, plus 9th and 13th"), + + '13susb9': ((C, F, G, Bb, Db+12, A+12), + (C, Db, F, F, G, A, Bb), + "7sus, plus flat 9th and 13th"), + + # these chords should probably NOT have the 5th included, + # but since a number of voicings depend on the 5th being + # the third note of the chord, they're here. + + '13': ((C, E, G, Bb, A+12), + (C, D, E, F, G, A, Bb), + "7th (including 5th) plus 13th (the 9th and 11th are not voiced)."), + + '13b5': ((C, E, Gb, Bb, A+12), + (C, D, E, F, Gb, A, Bb), + "7th with flat 5th, plus 13th (the 9th and 11th are not voiced)."), + + '13#9': ((C, E, G, Bb, Ds+12, A+12), + (C, Ds, E, F, G, A, Bb), + "7th (including 5th) plus 13th and sharp 9th (11th not voiced)."), + + '13b9': ((C, E, G, Bb, Db+12, A+12), + (C, Db, E, F, G, A, Bb), + "7th (including 5th) plus 13th and flat 9th (11th not voiced)."), + + 'M13': ((C, E, G, B, A+12), + (C, D, E, F, G, A, B), + "Major 7th (including 5th) plus 13th (9th and 11th not voiced)."), + + 'm13': ((C, Eb, G, Bb, A+12), + (C, D, Eb, F, G, A, Bb), + "Minor 7th (including 5th) plus 13th (9th and 11th not voiced)."), + + '13#11': ((C, E, G, Bb, Fs+12, A+12), + (C, D, E, Fs, G, A, Bb), + "7th plus sharp 11th and 13th (9th not voiced)."), + + 'M13#11': ((C, E, G, B, Fs+12, A+12), + (C, D, E, Fs, G, A, B), + "Major 7th plus sharp 11th and 13th (9th not voiced)."), + + # Because some patterns assume that the 3rd note in a chord is a 5th, + # or a varient, we duplicate the root into the position of the 3rd ... and + # to make the sound even we duplicate the 5th into the 4th position as well. + + '5': ((C, C, G, G ), + (C, D, E, F, G, A, B), + "Altered Fifth or Power Chord; root and 5th only."), + + 'omit3add9': ((C, C, G, D+12), + (C, D, E, F, G, A, Bb), + "Triad: root, 5th and 9th."), + + '7omit3': ((C, C, G, Bb), + (C, D, E, F, G, A, Bb), + "7th with unvoiced 3rd."), + + 'm7omit5': ((C, Eb, Bb), + (C, D, Eb, F, G, A, Bb), + "Minor 7th with unvoiced 5th."), +} + + +""" Extend our table with common synomyns. These are real copies, + not pointers. This is done so that a user redefine only affects + the original. +""" + +aliases = ( + ('aug9', '9#5', ''), + ('+9', '9#5', ''), + ('+9M7', 'aug9M7', ''), + ('+M7', 'M7#5', ''), + ('m(add9)', 'm(sus9)', ''), + ('69', '6(add9)', ''), + ('m69', 'm6(add9)', ''), + ('m(b5)', 'mb5', ''), + ('m7(b9)', 'm7b9', ''), + ('m7(#9)', 'm7#9', ''), + ('9+5', '9#5', ''), + ('m+5', 'm#5', ''), + ('M6', '6', ''), + ('m7-5', 'm7b5', ''), + ('m7(omit5)','m7omit5', ''), + ('+', 'aug', ''), + ('+7', 'aug7', ''), + ('7(omit3)', '7omit3', ''), + ('#5', 'aug', ''), + ('7#5b9', 'aug7b9', ''), + ('7-9', '7b9', ''), + ('7+9', '7#9', ''), + ('maj7', 'M7', ''), + ('M7-5', 'M7b5', ''), + ('M7+5', 'M7#5', ''), + ('M7(add13)','13b9', ''), + ('7alt', '7b5b9', ''), + ('7sus4', '7sus', ''), + ('7+', 'aug7', ''), + ('7#5', 'aug7', ''), + ('7+5', 'aug7', ''), + ('7-5', '7b5', ''), + ('sus', 'sus4', ''), + ('maj9', 'M9', ''), + ('maj13', 'M13', ''), + ('m(maj7)', 'mM7', ''), + ('m+7', 'mM7', ''), + ('min(maj7)','mM7', ''), + ('min#7', 'mM7', ''), + ('m#7', 'mM7', ''), + ('dim', 'dim7', 'A dim7, not a triad!'), + ('9sus', 'sus9', ''), + ('9-5', '9b5', ''), + ('dim3', 'mb5', 'Diminished triad (non-standard notation).'), + ('omit3(add9)','omit3add9', ''), + ('9sus4', 'sus9', '') + ) + +for a,b,d in aliases: + n=chordlist[b][0] + s=chordlist[b][1] + if not d: + d=chordlist[b][2] + + chordlist[a] = (n, s, d) + diff --git a/src/main/python/linuxband/mma/grooves.py b/src/main/python/linuxband/mma/grooves.py new file mode 100644 index 0000000..2dab9d8 --- /dev/null +++ b/src/main/python/linuxband/mma/grooves.py @@ -0,0 +1,166 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import pickle +import logging +import fnmatch +import os +from linuxband.glob import Glob +from linuxband.mma.parse import parse +from linuxband.mma.bar_info import BarInfo + + +class Grooves(object): + + __grooves_cache_file = Glob.CONFIG_DIR + '/grooves.cache' + + def __init__(self, config): + self.__config = config + self.__grooves_model = None + + def load_grooves(self, use_cache): + self.__grooves_model = None + if use_cache: + self.__grooves_model = self.__load_grooves_from_cache() + if not self.__grooves_model: + self.__grooves_model, grooves_list = self.__load_grooves() + self.__cache_grooves(grooves_list) + + def get_grooves_model(self): + return self.__grooves_model + + def __groove_compare(self, x, y): + a = x[0].upper() + b = y[0].upper() + if a > b: + return 1 + elif a == b: + return 0 + else: # x +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This file includes code found in MMA's 12.01 parse.py file, +written by Bob van der Poel + +This module does all file parsing. Most commands +are passed to the track classes; however, things +like TIME, SEQRND, etc. which just set global flags +are completely handled here. + +""" + +from linuxband.glob import Glob +from linuxband.mma.bar_chords import BarChords +from linuxband.mma.bar_info import BarInfo +from linuxband.mma.song_data import SongData + +######################################## +# File processing. Mostly jumps to pats +######################################## + +def parse(inpath): + """ + Process a mma input file. + """ + song_bar_info = [] + song_bar_chords = [] + song_bar_count = 0 + bar_number = 0 + bar_info = BarInfo() + bar_chords = BarChords() + + while True: + curline = inpath.readline() + + # EOF + if not curline: + song_bar_info.append(bar_info) # song_bar_info has always one element more then song_bar_chords + song_bar_count = bar_number + return SongData(song_bar_info, song_bar_chords, song_bar_count) + + """ convert 0xa0 (non-breakable space) to 0x20 (regular space). + """ + curline = curline.replace('\xa0', '\x20') + + # empty line + if curline.rstrip('\n').strip() == '': + bar_info.add_line([Glob.A_UNKNOWN, curline]); + continue + + l = curline.split() + + # line beginning with macro + if l[0][0] == '$': + wline = get_wrapped_line(inpath, curline) + wline.insert(0, Glob.A_UNKNOWN) + bar_info.add_line(wline) + continue + + + """ Handle BEGIN and END here. This is outside of the Repeat/End + and variable expand loops so SHOULD be pretty bullet proof. + Note that the beginData stuff is global to this module ... the + Include/Use directives check to make sure we're not doing that + inside a Begin/End. + + beginData[] is a list which we append to as more Begins are + encountered. + + The placement here is pretty deliberate. Variable expand comes + later so you can't macroize BEGIN ... I think this makes sense. + + The tests for 'begin', 'end' and the appending of the current + begin[] stuff have to be here, in this order. + """ + + action = l[0].upper() # 1st arg in line + + # parse BEGIN and END block + if action == 'BEGIN': + block_action = l[1].upper() + begin_block = parse_begin_block(inpath, curline) + if block_action in supported_block_actions: + tokens = parse_supported_block_action(block_action, begin_block) + begin_block = tokens + begin_block.insert(0, Glob.A_BEGIN_BLOCK) + begin_block.insert(1, block_action) + bar_info.add_line(begin_block) + continue + + # parse MSET block + if action == 'MSET': + mset_block = parse_mset_block(inpath, curline) + mset_block.insert(0, Glob.A_UNKNOWN) + bar_info.add_line(mset_block) + continue + + # parse IF - ENDIF block + if action == 'IF': + if_block = parse_if_block(inpath, curline) + if_block.insert(0, Glob.A_UNKNOWN) + bar_info.add_line(if_block) + continue + + # supported commands + if action in supported_actions: + wline = get_wrapped_line_join(inpath, curline) + tokens = parse_supported_action(action, wline) + tokens.insert(0, action) + bar_info.add_line(tokens) + continue + + # if the command is in the simple function table + if action in simple_funcs: + wline = get_wrapped_line(inpath, curline) + wline.insert(0, Glob.A_UNKNOWN) + bar_info.add_line(wline) + continue + + """ We have several possibilities ... + 1. The command is a valid assigned track name, + 2. The command is a valid track name, but needs to be + dynamically allocated, + 3. It's really a chord action + """ + + # track function BASS/DRUM/APEGGIO/CHORD ... + if '-' in action: + trk_class, ext = action.split('-', 1) #@UnusedVariable + else: + trk_class = action + + if trk_class in trk_classes: + # parsing track sequence ? + parse_seq = len(l) >= 1 and l[1].upper() == 'SEQUENCE' + wline = [] + while True: + wline.extend(get_wrapped_line(inpath, curline)) + if not parse_seq: break + """ Count the number of { and } and if they don't match read more lines and + append. If we get to the EOF then we're screwed and we error out. """ + wline2 = ''.join(wline) + if wline2.count('{') == wline2.count('}'): break + curline = inpath.readline() + if not curline: + raise ValueError("Reached EOF, Sequence {}s do not match") + wline.insert(0, Glob.A_UNKNOWN) + bar_info.add_line(wline) + continue + + # join the wrapped line into one line + wline = get_wrapped_line_join(inpath, curline) + + if wline[0].replace('\\\n', '').strip() == '': + # line is a comment or empty wrapped line + act = Glob.A_REMARK if wline[1].strip() else Glob.A_UNKNOWN + bar_info.add_line([act , wline[0], wline[1]]) + continue + + l, eol = wline + ### Gotta be a chord data line! + + """ A data line can have an optional bar number at the start + of the line. Makes debugging input easier. The next + block strips leading integers off the line. Note that + a line number on a line by itself it okay. + """ + + before_number = '' + if action.isdigit(): # isdigit() matches '1', '1234' but not '1a'! + l2 = l.lstrip() + before_number_len = len(l) - len(l2) + before_number = l[0:before_number_len] + l = l2 + numstr = l.split()[0] + bar_chords.set_number(int(numstr)) + l = l[len(numstr):] # remove number + if len(l.strip()) == 0: # ignore empty lines + bar_info.add_line([ Glob.A_UNKNOWN, wline[0] + wline[1] ]) + continue + + """ We now have a valid line. It'll look something like: + + 'Cm', '/', 'z', 'F#@4.5' { lyrics } [ solo ] * 2 + + + Special processing in needed for 'z' options in chords. A 'z' can + be of the form 'CHORDzX', 'z!' or just 'z'. + """ + after_number = None + last_chord = [] + ctable = [] + i = 0 + solo_count = 0 + lyrics_count = 0 + mismatched_solo = "Mismatched {}s for solo found in chord line" + mismatched_lyrics = "Mismatched []s for lyrics found in chord line" + while True: + chars = '' + while i < len(l): + ch = l[i] + if ch == '{': + """ Extract solo(s) from line ... this is anything in {}s. + The solo data is pushed into RIFFs and discarded from + the current line. + """ + solo_count += 1 + elif ch == '[': + """ Set lyrics from [stuff] in the current line. + NOTE: lyric.extract() inserts previously created + data from LYRICS SET and inserts the chord names + if that flag is active. + + """ + lyrics_count += 1 + elif ch == '}': + solo_count -= 1 + if solo_count < 0: + raise ValueError(mismatched_solo) + elif ch == ']': + lyrics_count -= 1 + if lyrics_count < 0: + raise ValueError(mismatched_lyrics) + elif ch == '*': + """ A bar can have an optional repeat count. This must + be at the end of bar in the form '* xx'. + """ + pass + elif ch in '\t\n\\ 0123456789': # white spaces, \ and repeat count + pass + elif solo_count == 0 and lyrics_count == 0: # found beginning of the chord + break + chars += ch + i += 1 + if i == len(l): # no more chord is coming + if solo_count != 0: + raise ValueError(mismatched_solo) + if lyrics_count != 0: + raise ValueError(mismatched_lyrics) + if after_number == None: + after_number = chars + else: + last_chord.append(chars) + ctable.append(last_chord) + break + else: # chord beginning + if after_number == None: + after_number = chars + else: + last_chord.append(chars) + ctable.append(last_chord) + chord_begin = i + # find the end of the chord + while i < len(l): + if l[i] in '{}[]*\t\n\\ ': + break + i += 1 + # chord examples: '/', 'z', 'Am7@2', 'Am6zC@3' + c = l[chord_begin:i] + last_chord = [ c ] + # the trailing string of the last chord can possibly include '\n' after which + # it would be difficult to add further chords. Therefore move the trailing string + # of the last chord to eol + eol = last_chord[1] + eol + last_chord[1] = '' + + bar_chords.set_before_number(before_number) + bar_chords.set_after_number(after_number) + bar_chords.set_eol(eol) + bar_chords.set_chords(ctable) + + song_bar_info.append(bar_info) + song_bar_chords.append(bar_chords) + + bar_number = bar_number + 1 + bar_info = BarInfo() + bar_chords = BarChords() + + +def get_wrapped_line(inpath, curline): + """ + Reads the whole wrapped line ('\' at the end) and stores it in a list. + + The lines in the list are not modified and are the same as in the file + """ + result = [] + while True: + if not curline: + raise ValueError("Reached EOF, the last line is not complete") + result.append(curline) + curline = curline.strip() + if not curline or not(curline[-1] == '\\'): + break + curline = inpath.readline() + return result + + +def get_wrapped_line_join(inpath, curline): + """ + Reads the wrapped line and joins it into one. + + Returns array of two strings: + 1) the line content which will be further parsed + 2) comment with '\n' at the end + If you join those strings you get exactly what was stored in the file + """ + wrapped = get_wrapped_line(inpath, curline) + line = '' + comment = '' + i = 0 + while i < len(wrapped): + l = wrapped[i] + if comment: + comment = comment + l + else: + if '//' in l: + l, comm = l.split('//', 1) + comment = '//' + comm + line = line + l + else: + line = line + l + i = i + 1 + return [ line, comment ] + + +def parse_begin_block(inpath, curline): + beginDepth = 1 + result = [ curline ] + while True: + curline = inpath.readline() + if not curline: + raise ValueError("Reached EOF while looking for End") + l = curline.split() + action = None + if len(l) > 0: + action = l[0].upper() + if action == 'BEGIN': + beginDepth = beginDepth + 1 + if action == 'END': + beginDepth = beginDepth - 1 + result.append(curline) + if beginDepth == 0: + break + return result + +def parse_mset_block(inpath, curline): + l = curline.split() + if len(l) < 2: + raise ValueError("Use: MSET VARIABLE_NAME MsetEnd") + result = [ curline ] + while True: + curline = inpath.readline() + if not curline: + raise ValueError("Reached EOF while looking for MSetEnd") + l = curline.split() + action = None + if len(l) > 0: + action = l[0].upper() + result.append(curline) + if action in ("MSETEND", 'ENDMSET'): + break + return result + +def parse_if_block(inpath, curline): + ifDepth = 1 + result = [ curline ] + while True: + curline = inpath.readline() + if not curline: + raise ValueError("Reached EOF while looking for EndIf") + l = curline.split() + action = None + if len(l) > 0: + action = l[0].upper() + if action == 'IF': + ifDepth = ifDepth + 1 + if action in ('ENDIF', 'IFEND'): + ifDepth = ifDepth - 1 + result.append(curline) + if ifDepth == 0: + break + return result + +def parse_supported_action(action, wline): + line = [] + if action == Glob.A_AUTHOR: # ['Author', ' Bob van der Poel\n'] + line = tokenize_line(wline[0], 1) + elif action == Glob.A_DEF_GROOVE: # ['DefGroove', ' ', 'ModernJazz', ' ModernJazz with just a piano and guitar.\n'] + line = tokenize_line(wline[0], 2) + elif action == Glob.A_GROOVE: # ['Groove', ' ', 'Tango', ' LightTango LightTangoSus LightTango\n'] + line = tokenize_line(wline[0], 2) + elif action == Glob.A_REPEAT: # nothing to parse + line = [ wline[0] ] + elif action == Glob.A_REPEAT_END: # ['RepeatEnd', ' ', '2', '\n'] or ['RepeatEnd', '\n' ] + line = tokenize_line(wline[0], 2) + elif action == Glob.A_REPEAT_ENDING: # + line = tokenize_line(wline[0], 2) + elif action == Glob.A_TEMPO: # ['Tempo', ' ', '120', '\n'] + line = tokenize_line(wline[0], 2) + elif action == Glob.A_TIME: # ['Time', ' ', '4'. '\n' ] + line = tokenize_line(wline[0], 2) + line.append(wline[1]) + return line + +def parse_supported_block_action(block_action, begin_block): + return [ begin_block[0], ''.join(begin_block[1:-1]), begin_block[-1] ] + +def tokenize_line(line, limit): + """ + Split the line into tokens and characters in between. + + Example: + ['Time', ' ', '4', '\n'] + ['Timesig', ' ', '4', ' ', '4', '\n'] + ['DefGroove', ' ', 'ModernJazz', ' ModernJazz with just a piano and guitar.\n'] + """ + chars_between = '\t\n\\ ' + tokenized_line = [] + count = 0 + start = 0 + end = 0 + read_token = True + while start < len(line): + if read_token: + while end < len(line) and line[end] not in chars_between: + end += 1 + tokenized_line.append(line[start:end]) + count += 1 + if count == limit: + tokenized_line.append(line[end:]) + break + else: + while end < len(line) and line[end] in chars_between: + end += 1 + tokenized_line.append(line[start:end]) + read_token = not read_token + start = end + return tokenized_line + +""" ================================================================= + + Command jump tables. These need to be at the end of this module + to avoid undefined name errors. The tables are only used in + the parse() function. + + The first table is for the simple commands ... those which DO NOT + have a leading track name. The second table is for commands which + require a leading track name. + + The alphabetic order is NOT needed, just convenient. + +""" + +simple_funcs = \ + 'ADJUSTVOLUME', \ + 'ALLGROOVES', \ + 'ALLTRACKS', \ + 'AUTHOR', \ + 'AUTOSOLOTRACKS', \ + 'BEATADJUST', \ + 'CHANNELPREF', \ + 'CHORDADJUST', \ + 'COMMENT', \ + 'CRESC', \ + 'CUT', \ + 'DEBUG', \ + 'DEC', \ + 'DECRESC', \ + 'DEFALIAS', \ + 'DEFCHORD', \ + 'DEFGROOVE', \ + 'DELETE', \ + 'DOC', \ + 'DOCVAR', \ + 'DRUMVOLTR', \ + 'ELSE', \ + 'ENDIF', \ + 'ENDMSET', \ + 'ENDREPEAT', \ + 'EOF', \ + 'FERMATA', \ + 'GOTO', \ + 'GROOVE', \ + 'GROOVECLEAR', \ + 'IF', \ + 'IFEND', \ + 'INC', \ + 'INCLUDE', \ + 'KEYSIG', \ + 'LABEL', \ + 'LYRIC', \ + 'MIDIDEF', \ + 'MIDI', \ + 'MIDICOPYRIGHT' \ + 'MIDICUE' \ + 'MIDIFILE', \ + 'MIDIINC', \ + 'MIDIMARK', \ + 'MIDISPLIT', \ + 'MIDITEXT' \ + 'MIDITNAME' \ + 'MMAEND', \ + 'MMASTART', \ + 'MSET', \ + 'MSETEND', \ + 'NEWSET', \ + 'PATCH', \ + 'PRINT', \ + 'PRINTACTIVE', \ + 'PRINTCHORD', \ + 'REPEAT', \ + 'REPEATEND', \ + 'REPEATENDING', \ + 'RESTART', \ + 'RNDSEED', \ + 'RNDSET', \ + 'SEQ', \ + 'SEQCLEAR', \ + 'SEQRND', \ + 'SEQRNDWEIGHT', \ + 'SEQSIZE', \ + 'SET', \ + 'SETAUTOLIBPATH', \ + 'SETINCPATH', \ + 'SETLIBPATH', \ + 'SETMIDIPLAYER', \ + 'SETOUTPATH', \ + 'SETSYNCTONE', \ + 'SHOWVARS', \ + 'STACKVALUE', \ + 'SWELL', \ + 'SWINGMODE', \ + 'SYNCHRONIZE', \ + 'TEMPO', \ + 'TIME', \ + 'TIMESIG', \ + 'TONETR', \ + 'TRUNCATE', \ + 'UNSET', \ + 'USE', \ + 'VARCLEAR', \ + 'VEXPAND', \ + 'VOICEVOLTR', \ + 'VOICETR', \ + 'VOLUME', \ + 'TRANSPOSE' + + +trackFuncs = \ + 'ACCENT', \ + 'ARPEGGIATE' \ + 'ARTICULATE' \ + 'CHANNEL', \ + 'DUPRIFF', \ + 'MIDIVOLUME', \ + 'MIDICRESC', \ + 'MIDIDECRESC', \ + 'CHSHARE', \ + 'COMPRESS', \ + 'COPY', \ + 'CRESC', \ + 'CUT', \ + 'DECRESC', \ + 'DELAY', \ + 'DIRECTION', \ + 'DRUMTYPE', \ + 'DUPROOT', \ + 'FORCEOUT', \ + 'GROOVE', \ + 'HARMONY', \ + 'HARMONYONLY', \ + 'HARMONYVOLUME', \ + 'INVERT', \ + 'LIMIT', \ + 'MALLET', \ + 'MIDICLEAR' \ + 'MIDICUE' \ + 'MIDIDEF', \ + 'MIDIGLIS', \ + 'MIDIPAN', \ + 'MIDISEQ', \ + 'MIDITEXT' \ + 'MIDITNAME', \ + 'MIDIVOICE', \ + 'OCTAVE', \ + 'OFF', \ + 'ON', \ + 'ORNAMENT' \ + 'TUNING' \ + 'CAPO' \ + 'RANGE', \ + 'RESTART', \ + 'RIFF', \ + 'RSKIP', \ + 'RTIME', \ + 'RVOLUME', \ + 'SCALETYPE', \ + 'SEQCLEAR', \ + 'SEQRND', \ + 'SEQUENCE', \ + 'SEQRNDWEIGHT', \ + 'SWELL', \ + 'MIDINOTE' \ + 'NOTESPAN', \ + 'STRUM', \ + 'TONE', \ + 'UNIFY', \ + 'VOICE', \ + 'VOICING', \ + 'VOLUME', \ + 'DEFINE' + +trk_classes = \ + 'BASS', \ + 'CHORD', \ + 'ARPEGGIO', \ + 'SCALE', \ + 'DRUM', \ + 'WALK', \ + 'MELODY', \ + 'SOLO', \ + 'ARIA', \ + 'PLECTRUM' + +supported_actions = \ + Glob.A_AUTHOR, \ + Glob.A_DEF_GROOVE, \ + Glob.A_GROOVE, \ + Glob.A_REPEAT, \ + Glob.A_REPEAT_END, \ + Glob.A_REPEAT_ENDING, \ + Glob.A_TEMPO, \ + Glob.A_TIME + +supported_block_actions = \ + Glob.A_DOC diff --git a/src/main/python/linuxband/mma/song.py b/src/main/python/linuxband/mma/song.py new file mode 100644 index 0000000..7ff5738 --- /dev/null +++ b/src/main/python/linuxband/mma/song.py @@ -0,0 +1,132 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import cStringIO +import logging +from linuxband.mma.parse import parse +from linuxband.mma.song_data import SongData +from linuxband.mma.bar_info import BarInfo + +class Song(object): + + def __init__(self, midi_generator): + self.__clear_song() + self.__midi_generator = midi_generator + + def get_data(self): + return self.__song_data + + def load_from_file(self, file_name): + logging.info("Loading file '%s'", file_name) + try: + mma_file = file(file_name, 'r') + try: + mma_data = mma_file.read() + finally: + mma_file.close() + except: + logging.exception("Unable to open '" + file_name + "' for input") + return -2 + self.__pending_mma_data = mma_data + self.__song_data.set_save_needed(False) + + def load_from_string(self, mma_data): + self.__pending_mma_data = mma_data + self.__song_data.set_save_needed(True) + + def compile_song(self): + if not self.__song_data.is_save_needed() and self.__pending_mma_data == None and self.__invalid_mma_data == None: + logging.debug('No compilation needed') + return self.__last_compile_result + if self.__pending_mma_data == None: + mma_data = self.write_to_string() + res = self.__midi_generator.check_mma_syntax(mma_data) + else: + res = self.__do_compile(self.__pending_mma_data) + self.__pending_mma_data = None + return res + + def write_to_mma_file(self, file_name): + mma_data = self.write_to_string() + self.__do_write_to_file(file_name, mma_data) + self.__song_data.set_save_needed(False) + + def write_to_midi_file(self, file_name): + mma_data = self.write_to_string() + res, midi = self.__midi_generator.generate_smf(mma_data) + if res == 0: self.__do_write_to_file(file_name, midi) + + def write_to_string(self): + """ + If the mma was parsed correctly return it. Otherwise give the invalid mma data back. + """ + if self.__invalid_mma_data == None: + return self.__song_data.write_to_string() + else: + return self.__invalid_mma_data + + def get_playback_midi_data(self): + """ + Get midi file which will be sent to the client. + + Than create mma file with markers for tracking and generate the resulting midi from it. + """ + mma_data_marks = self.__song_data.write_to_string_with_midi_marks() + return self.__midi_generator.generate_smf(mma_data_marks) + + def __clear_song(self): + """ + Called when parsing of mma data failed. + + E.g. during opening the new file or parsing data from the source editor. + The song must have minimum one BarInfo on which the cursor is located. + + bar_count = number of bar_chords in the song, number of bar_info is bar_count + 1 + """ + self.__song_data = SongData([ BarInfo() ], [], 0) + self.__invalid_mma_data = None + self.__pending_mma_data = None + self.__last_compile_result = None + + def __do_compile(self, mma_data): + res = self.__last_compile_result = self.__midi_generator.check_mma_syntax(mma_data) + if res == 0: + mma_file = cStringIO.StringIO(mma_data) + try: + self.__song_data = parse(mma_file) + except ValueError: + logging.exception("Failed to parse the file.") + res = -1 + mma_file.close() + if res > 0 or res == -1: + self.__clear_song() + self.__invalid_mma_data = mma_data + self.__song_data.set_save_needed(True) + else: + self.__invalid_mma_data = None + return res + + def __do_write_to_file(self, file_name, data): + logging.info('Opening output file %s', file_name) + try: + f = file(file_name, 'w') + try: + f.write(data) + finally: + f.close() + except: + logging.exception("Failed to save data to '" + file_name + "'.") diff --git a/src/main/python/linuxband/mma/song_data.py b/src/main/python/linuxband/mma/song_data.py new file mode 100644 index 0000000..51a9c68 --- /dev/null +++ b/src/main/python/linuxband/mma/song_data.py @@ -0,0 +1,174 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import copy +import logging +from linuxband.mma.bar_info import BarInfo +from linuxband.mma.bar_chords import BarChords +from linuxband.glob import Glob + +class SongData(object): + + # how many additional lines are needed for one bar in the mma file + # used for line tracking when playing midi + __LINES_PER_BAR = 5 + __LINES_ADD = 1 + + def __init__(self, bar_info, bar_chords, bar_count): + self.__bar_info = bar_info + self.__bar_chords = bar_chords + self.__bar_count = bar_count + self.__beats_per_bar = 4 # TODO Beats/bar, set with TIME + self.__save_needed = False + for bar_info in self.__bar_info: + bar_info.set_song_data(self) + for bar_chord in self.__bar_chords: + bar_chord.set_song_data(self) + + def get_bar_info_all(self): + return self.__bar_info + + def get_bar_count(self): + return self.__bar_count + + def get_beats_per_bar(self): + return self.__beats_per_bar + + def set_bar_info(self, bar_num, bar_info): + self.__bar_info[bar_num] = copy.deepcopy(bar_info) + self.__save_needed = True + + def get_bar_info(self, bar_num): + return self.__bar_info[bar_num] + + def set_bar_chords(self, bar_num, bar_chords): + """ Replaces chords in the bar and fixes the bar number. """ + bar_chords = self.__bar_chords[bar_num] = copy.deepcopy(bar_chords) + if bar_num > 0: + prev = self.__bar_chords[bar_num - 1].get_number() + if prev: + bar_chords.set_number(prev + 1) + self.__save_needed = True + + def get_bar_chords(self, bar_num): + return self.__bar_chords[bar_num] + + def set_save_needed(self, save_needed): + self.__save_needed = save_needed + + def is_save_needed(self): + return self.__save_needed + + def changed(self): + logging.debug('Song changed'); + self.__save_needed = True + + def create_bar_info(self): + bar_info = BarInfo() + bar_info.set_song_data(self) + return bar_info + + def create_bar_chords(self): + bar_chords = BarChords() + bar_chords.set_song_data(self) + return bar_chords + + def change_bar_count(self, new_bar_count): + bar_info = self.__bar_info + bar_chords = self.__bar_chords + bar_count = self.__bar_count + if new_bar_count > bar_count: + i = 0 + diff = new_bar_count - bar_count + while i < diff: + # append None and then call set_bar_chords method which computes the bar number + bar_chords.append(None) + self.set_bar_chords(len(bar_chords) - 1, self.create_bar_chords()) + bar_info.append(self.create_bar_info()) + self.__bar_count += 1 + i = i + 1 + elif new_bar_count < bar_count: + i = 0 + diff = bar_count - new_bar_count + while i < diff: + bar_chords.pop() + bar_info.pop() + self.__bar_count -= 1 + i = i + 1 + self.__save_needed = True + + def get_title(self): + lines = self.__bar_info[0].get_lines() + if len(lines) > 0: + if lines[0][0] == Glob.A_REMARK: + comm = lines[0][-1].strip() + comm = comm [2:] # remove '//' + return comm.strip() + return Glob.UNTITLED_SONG_NAME + + def set_title(self, title): + bar_info = self.__bar_info[0] + # get first line + lines = bar_info.get_lines() + if len(lines) == 0 or lines[0][0] <> Glob.A_REMARK: + line = [Glob.A_REMARK, ""] + bar_info.insert_line(line) + else: + line = lines[0] + line[-1] = "// " + title + "\n" + self.__save_needed = True + + def write_to_string(self): + mma_array = [] + for i in range(0, self.__bar_count): + mma_array.extend(self.__bar_info[i].get_as_string_list()) + mma_array.extend(self.__bar_chords[i].get_as_string_list()) + mma_array.extend(self.__bar_info[self.__bar_count].get_as_string_list()) + return ''.join(mma_array) + + def write_to_string_with_midi_marks(self): + """ + Write the mma file which will be compiled by mma and played in midi player. + + We use macros to wrap chords. It allows the tracking of which bar is played. + """ + mma_array = [] + # write header with macro definitions + for i in range(0, self.__bar_count): + mma_array.extend("MSet MacroBar%i\n" % i) + mma_array.extend("MidiMark BAR%i\n" % i) + mma_array.extend("MidiMark $_LineNum\n") + mma_array.extend(self.__bar_chords[i].get_as_string_list()) + if i == self.__bar_count - 1: + mma_array.extend("MidiMark END\n") + mma_array.extend("MSetEnd\n") # 5 lines + # write the song + for i in range(0, self.__bar_count): + mma_array.extend(self.__bar_info[i].get_as_string_list()) + mma_array.extend("$MacroBar%i\n" % i) + mma_array.extend(self.__bar_info[self.__bar_count].get_as_string_list()) + return ''.join(mma_array) + + def write_tokens_debug(self): + """ For debugging purposes. """ + for i in range(0, self.__bar_count): + self.__bar_info[i].show_debug() + self.__bar_chords[i].show_debug() + self.__bar_info[self.__bar_count].show_debug() + + def get_mma_line_offset(self): + return self.__bar_count * SongData.__LINES_PER_BAR + SongData.__LINES_ADD diff --git a/src/main/resources/error-pointer.png b/src/main/resources/error-pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..70eb997de8e57d775f35c5a6866a3f6ad3468b13 GIT binary patch literal 710 zcmV;%0y+JOP)E_(0f!EO zIEbsz+LnSMg%)a+wts{U=2fIqLt9HhL>vS~ba9LdqG$;9#rKk!_rB91rkXUJ`oUf9 zJ?GrZ{mvDV_@8H1N`9)YexZGT{%%ewH66+4KmG+KSrV3MK3f|c%=pL0)aU19GD-jN zkgMUaW0~g5a(p@Y>Cb_IjGsu5+uP*E2D!OO?(fr2B>1DR&#_E%>Mlo$JFu#%o;(bP z=Z9jk`|9EXux&u8;+Q~P9dttj)}m22xx4!|lF!cpC)Yn6|v|xRF+`ZV@c^C1#Ss{s-VO&4n+Nv^+j=)6QWJW197bSDI9#CrLdobv@D=V}|N0|zR znCk9ks;i5s&Q986V{~FMer;`aEv4R6Hj>EeZ^5ABSQdYcj4&ApFxlM9%V?BWi;Ij0 z0Yq-$5t&_WZ+G0~W#-~>=HhW)EiCX$QlE^&(QoOk+nX82R4eGzNa=#o>od5s;07*qoM6N<$f|@%<*#H0l literal 0 HcmV?d00001 diff --git a/src/main/resources/line-pointer.png b/src/main/resources/line-pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..ca8d86d4d44454fdcc18a8e0bf9fd385b70ba0fb GIT binary patch literal 601 zcmV-f0;c_mP)*8O|pfiP!;hDcoVUB^B|)5=Ol`B z5us87B0@j~=|S*Np$Kh2t34@J2&MT3+G;?NegkhF)lvzS>}+CQbeD(>13nC4GwJk6)m?qKAGBR_Nu;H>ccUMJ0c(zp0pSytPH^*#oX`Obk zj&MALNMX@nBCKg(^-qkhlX8!tf;`p74^$L{=Te=wSZgq?uSE3)FcDyOZz2&wcH>Ia zd%TH}VmK_>W%Yl4z4K80#}bPY>>_=tF?C7S|d) zqbcZwiJk3s4neQ^V#mB2p8e-uoO0~emy&%klWi +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest +from linuxband.mma.bar_chords import BarChords +from linuxband.mma.bar_info import BarInfo +from linuxband.mma.song_data import SongData + + +class TestBarChords(unittest.TestCase): + + def setUp(self): + song_data = self.__song_data = SongData([ BarInfo() ], [], 0) + self.__bar_chords1 = BarChords() + self.__bar_chords1.set_song_data(song_data) + self.__bar_chords1.set_chords([['CM7', ' trail1 '], [ 'Am', ' trail2 ']]) + self.__bar_chords2 = BarChords() + self.__bar_chords2.set_song_data(song_data) + self.__bar_chords2.set_chords([['CM7', ' trail1 '], [ 'Am', ' trail2 '], [ '/', ' '], ['G', ' trail3 ']]) + + def test_set_chord1(self): + bar_chords = self.__bar_chords1 + bar_chords.set_chord(0, 'A') + assert bar_chords.get_chords() == [['A', ' trail1 '], [ 'Am', ' trail2 ']] + + bar_chords.set_chord(0, '') + assert bar_chords.get_chords() == [['/', ' trail1 '], [ 'Am', ' trail2 ']] + + # remove the last chord + assert '\n' == bar_chords.get_eol() + bar_chords.set_chord(1, '') + assert bar_chords.get_chords() == [['/', ' trail1 ']] + assert ' trail2 \n' == bar_chords.get_eol() + + bar_chords.set_chord(0, '') + assert bar_chords.get_chords() == [['/', ' trail1 ']] + + bar_chords.set_chord(3, 'BM6') + assert bar_chords.get_chords() == [['/', ' trail1 '], ['/', ' '], ['/', ' '], ['BM6', '']] + + def test_set_chord2(self): + bar_chords = self.__bar_chords2 + + bar_chords.set_chord(2, 'B') + assert bar_chords.get_chords() == [['CM7', ' trail1 '], [ 'Am', ' trail2 '], [ 'B', ' '], ['G', ' trail3 ']] + + bar_chords.set_chord(0, '') + assert bar_chords.get_chords() == [['/', ' trail1 '], [ 'Am', ' trail2 '], [ 'B', ' '], ['G', ' trail3 ']] + + # remove the last chord + assert '\n' == bar_chords.get_eol() + bar_chords.set_chord(3, '') + assert bar_chords.get_chords() == [['/', ' trail1 '], [ 'Am', ' trail2 '], [ 'B', ' ']] + assert ' trail3 \n' == bar_chords.get_eol() + +if __name__ == '__main__': + # When this module is executed from the command-line, run all its tests + unittest.main() + diff --git a/src/test/python/linuxband/mma/test_parse.py b/src/test/python/linuxband/mma/test_parse.py new file mode 100644 index 0000000..35ac21f --- /dev/null +++ b/src/test/python/linuxband/mma/test_parse.py @@ -0,0 +1,80 @@ +# Copyright (c) 2012 Ales Nosek +# +# This file is part of LinuxBand. +# +# LinuxBand is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import logging +from linuxband.logger import Logger +from linuxband.mma.parse import parse + + +class TestParse(object): + + def test_parse(self, input_name): + song_data = self.__load_file(input_name) + if not song_data: return -1 + mma_out = song_data.write_to_string() + output_name = input_name + ".out" + try: + fout = open(output_name, 'w') + fout.write(mma_out) + fout.close() + except: + logging.exception("Failed to save data to '" + output_name + "'.") + return -1 + logging.info('Written file %s' % output_name) + return 0 + + def test_tokens(self, input_name): + song_data = self.__load_file(input_name) + if not song_data: return -1 + song_data.write_tokens_debug() + return 0 + + def test_midi_marks(self, input_name): + song_data = self.__load_file(input_name) + if not song_data: return -1 + logging.info('\n%s' % song_data.write_to_string_with_midi_marks()) + return 0 + + def __load_file(self, input_name): + logging.info('Parsing file %s' % input_name) + try: + fin = file(input_name, 'r') + song_data = parse(fin) + fin.close() + except: + logging.exception("Unable to open '" + input_name + "' for input") + return None + return song_data + + +def main(): + Logger.initLogging() + if len(sys.argv) > 1: + if sys.argv[1] == "--tokens": + file_name = sys.argv[2] + ret = TestParse().test_tokens(file_name) + elif sys.argv[1] == "--midimarks": + file_name = sys.argv[2] + ret = TestParse().test_midi_marks(file_name) + else: + file_name = sys.argv[1] + ret = TestParse().test_parse(file_name) + sys.exit(ret) + +if __name__ == "__main__": + main()