diff --git a/openlp/plugins/songs/forms/editverseform.py b/openlp/plugins/songs/forms/editverseform.py index d6ce2c8f4..24c026320 100644 --- a/openlp/plugins/songs/forms/editverseform.py +++ b/openlp/plugins/songs/forms/editverseform.py @@ -25,7 +25,7 @@ import logging from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.plugins.songs.lib import VerseType +from openlp.plugins.songs.lib import VerseType, transpose_lyrics from .editversedialog import Ui_EditVerseDialog log = logging.getLogger(__name__) @@ -101,13 +101,15 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog): """ The transpose up button clicked """ - print('...') + transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1) + self.verse_text_edit.setPlainText(transposed_lyrics) def on_transepose_down_button_clicked(self): """ The transpose down button clicked """ - print('...') + transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), -1) + self.verse_text_edit.setPlainText(transposed_lyrics) def update_suggested_verse_number(self): """ diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index 0de73baa2..406dfa695 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -29,7 +29,7 @@ import re from PyQt5 import QtWidgets -from openlp.core.common import AppLocation +from openlp.core.common import AppLocation, Settings from openlp.core.lib import translate from openlp.core.utils import CONTROL_CHARS from openlp.plugins.songs.lib.db import MediaFile, Song @@ -521,18 +521,6 @@ def strip_rtf(text, default_encoding=None): return text, default_encoding -def transpose_lyrics(lyric, transepose_value): - """ - Transepose lyrics - - :param lyrcs: The lyrics to be transposed - :param transepose_value: The value to transpose the lyrics with - :return: The transposed lyrics - """ - if '[' not in lyrics: - return lyrics - - def delete_song(song_id, song_plugin): """ Deletes a song from the database. Media files associated to the song are removed prior to the deletion of the song. @@ -554,3 +542,107 @@ def delete_song(song_id, song_plugin): except OSError: log.exception('Could not remove directory: %s', save_path) song_plugin.manager.delete_object(Song, song_id) + + +def transpose_lyrics(lyrics, transepose_value): + """ + Transepose lyrics + + :param lyrcs: The lyrics to be transposed + :param transepose_value: The value to transpose the lyrics with + :return: The transposed lyrics + """ + if '[' not in lyrics: + return lyrics + # Split the lyrics based on chord tags + lyric_list = re.split('(\[|\]|/)', lyrics) + transposed_lyrics = '' + in_tag = False + notation = Settings().value('songs/chord notation') + for word in lyric_list: + if not in_tag: + transposed_lyrics += word + if word == '[': + in_tag = True + else: + if word == ']': + in_tag = False + transposed_lyrics += word + elif word == '/': + transposed_lyrics += word + else: + # This MUST be a chord + transposed_lyrics += transpose_chord(word, transepose_value, notation) + # If still inside a chord tag something is wrong! + if in_tag: + return lyrics + else: + return transposed_lyrics + +def transpose_chord(chord, transpose_value, notation): + """ + Transpose chord according to the notation used. + NOTE: This function has a javascript equivalent in chords.js - make sure to update both! + + :param chord: The chord to transpose. + :param transpose_value: The value the chord should be transposed. + :param notation: The notation to use when transposing. + :return: The transposed chord. + """ + # See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale + notes_sharp_notation = {} + notes_flat_notation = {} + notes_sharp_notation['german'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'] + notes_flat_notation['german'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'] + notes_sharp_notation['english'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'] + notes_flat_notation['english'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','Bb','B'] + notes_sharp_notation['neo-latin'] = ['Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#','La','La#','Si'] + notes_flat_notation['neo-latin'] = ['Do','Reb','Re','Mib','Fab','Fa','Solb','Sol','Lab','La','Sib','Si'] + chord_split = chord.replace('♭', 'b').split('/[\/\(\)]/') + transposed_chord = '' + note = '' + notenumber = -1 + rest = '' + current_chord = '' + last_chord = '' + notes_sharp = notes_sharp_notation[notation] + notes_flat = notes_flat_notation[notation] + notes_preferred = ['b','#','#','#','#','#','#','#','#','#','#','#'] + chord_notes = [] + for i in range(0, len(chord_split)): + if i > 0: + transposed_chord += '/' + currentchord = chord_split[i] + if currentchord[0] == '(': + transposed_chord += '(' + if len(currentchord) > 1: + currentchord = currentchord[1:] + else: + currentchord = "" + if len(currentchord) > 0: + if len(currentchord) > 1: + if '#b'.find(currentchord[1]) == -1: + note = currentchord[0:1] + rest = currentchord[1:] + else: + note = currentchord[0:2] + rest = currentchord[2:] + else: + note = currentchord + rest = '' + notenumber = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note) + notenumber += transpose_value + while notenumber > 11: + notenumber -= 12 + while notenumber < 0: + notenumber += 12 + if i == 0: + current_chord = notes_sharp[notenumber] if notes_preferred[notenumber] == '#' else notes_flat[notenumber] + last_chord = current_chord + else: + current_chord = notes_flat[notenumber] if last_chord not in notes_sharp else notes_sharp[notenumber] + if not (note not in notes_flat and note not in notes_sharp): + transposed_chord += current_chord + rest + else: + transposed_chord += note + rest + return transposed_chord diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index 8cca502b0..6d7ac25e8 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -24,7 +24,7 @@ This module contains tests for the lib submodule of the Songs plugin. """ from unittest import TestCase -from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf +from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf, transpose_chord from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length from tests.functional import patch, MagicMock @@ -264,6 +264,32 @@ class TestLib(TestCase): # THEN: The stripped text matches thed expected result assert result == exp_result, 'The result should be %s' % exp_result + def transpose_chord_up_test(self): + """ + Test that the transpose_chord() method works when transposing up + """ + # GIVEN: A Chord + chord = 'C' + + # WHEN: Transposing it 1 up + new_chord = transpose_chord(chord, 1, 'english') + + # THEN: The chord should be transposed up one note + self.assertEqual(new_chord, 'C#', 'The chord should be transposed up.') + + def transpose_chord_down_test(self): + """ + Test that the transpose_chord() method works when transposing down + """ + # GIVEN: A Chord + chord = 'C' + + # WHEN: Transposing it 1 down + new_chord = transpose_chord(chord, -1, 'english') + + # THEN: The chord should be transposed down one note + self.assertEqual(new_chord, 'B', 'The chord should be transposed down.') + class TestVerseType(TestCase): """