import attr
from . import models as gp
from . import gp3
from .utils import clamp
[docs]class GP4File(gp3.GP3File):
"""A reader for GuitarPro 4 files."""
# Reading
# =======
[docs] def readSong(self):
"""Read the song.
A song consists of score information, triplet feel, lyrics,
tempo, song key, MIDI channels, measure and track count, measure
headers, tracks, measures.
- Version: :ref:`byte-size-string` of size 30.
- Score information.
See :meth:`readInfo`.
- Triplet feel: :ref:`bool`.
If value is true, then triplet feel is set to eigth.
- Lyrics. See :meth:`readLyrics`.
- Tempo: :ref:`int`.
- Key: :ref:`int`. Key signature of the song.
- Octave: :ref:`signed-byte`. Reserved for future uses.
- MIDI channels. See :meth:`readMidiChannels`.
- Number of measures: :ref:`int`.
- Number of tracks: :ref:`int`.
- Measure headers. See :meth:`readMeasureHeaders`.
- Tracks. See :meth:`readTracks`.
- Measures. See :meth:`readMeasures`.
"""
song = gp.Song(tracks=[], measureHeaders=[])
song.version = self.readVersion()
song.versionTuple = self.versionTuple
song.clipboard = self.readClipboard()
self.readInfo(song)
self._tripletFeel = gp.TripletFeel.eighth if self.readBool() else gp.TripletFeel.none
song.lyrics = self.readLyrics()
song.tempo = self.readInt()
song.key = gp.KeySignature((self.readInt(), 0))
self.readSignedByte() # octave
channels = self.readMidiChannels()
measureCount = self.readInt()
trackCount = self.readInt()
with self.annotateErrors('reading'):
self.readMeasureHeaders(song, measureCount)
self.readTracks(song, trackCount, channels)
self.readMeasures(song)
return song
def readClipboard(self):
if not self.isClipboard():
return
clipboard = gp.Clipboard()
clipboard.startMeasure = self.readInt()
clipboard.stopMeasure = self.readInt()
clipboard.startTrack = self.readInt()
clipboard.stopTrack = self.readInt()
return clipboard
def isClipboard(self):
return self.version.startswith('CLIPBOARD')
[docs] def readLyrics(self):
"""Read lyrics.
First, read an :ref:`int` that points to the track lyrics are
bound to. Then it is followed by 5 lyric lines. Each one
constists of number of starting measure encoded in :ref:`int`
and :ref:`int-size-string` holding text of the lyric line.
"""
lyrics = gp.Lyrics()
lyrics.trackChoice = self.readInt()
for line in lyrics.lines:
line.startingMeasure = self.readInt()
line.lyrics = self.readIntSizeString()
return lyrics
def packMeasureHeaderFlags(self, header, previous=None):
flags = super().packMeasureHeaderFlags(header, previous)
if previous is None or header.keySignature != previous.keySignature:
flags |= 0x40
if header.hasDoubleBar:
flags |= 0x80
return flags
def writeMeasureHeaderValues(self, header, flags):
super().writeMeasureHeaderValues(header, flags)
if flags & 0x40:
self.writeSignedByte(header.keySignature.value[0])
self.writeSignedByte(header.keySignature.value[1])
[docs] def readNewChord(self, chord):
"""Read new-style (GP4) chord diagram.
New-style chord diagram is read as follows:
- Sharp: :ref:`bool`. If true, display all semitones as sharps,
otherwise display as flats.
- Blank space, 3 :ref:`Bytes <byte>`.
- Root: :ref:`byte`. Values are:
* -1 for customized chords
* 0: C
* 1: C#
* ...
- Type: :ref:`byte`. Determines the chord type as followed. See
:class:`guitarpro.models.ChordType` for mapping.
- Chord extension: :ref:`byte`. See
:class:`guitarpro.models.ChordExtension` for mapping.
- Bass note: :ref:`int`. Lowest note of chord as in *C/Am*.
- Tonality: :ref:`int`. See
:class:`guitarpro.models.ChordAlteration` for mapping.
- Add: :ref:`bool`. Determines if an "add" (added note) is
present in the chord.
- Name: :ref:`byte-size-string`. Max length is 22.
- Fifth tonality: :ref:`byte`. Maps to
:class:`guitarpro.models.ChordExtension`.
- Ninth tonality: :ref:`byte`. Maps to
:class:`guitarpro.models.ChordExtension`.
- Eleventh tonality: :ref:`byte`. Maps to
:class:`guitarpro.models.ChordExtension`.
- List of frets: 6 :ref:`Ints <int>`. Fret values are saved as
in default format.
- Count of barres: :ref:`byte`. Maximum count is 5.
- Barre frets: 5 :ref:`Bytes <byte>`.
- Barre start strings: 5 :ref:`Bytes <byte>`.
- Barre end string: 5 :ref:`Bytes <byte>`.
- Omissions: 7 :ref:`Bools <bool>`. If the value is true then
note is played in chord.
- Blank space, 1 :ref:`byte`.
- Fingering: 7 :ref:`SignedBytes <signed-byte>`. For value
mapping, see :class:`guitarpro.models.Fingering`.
"""
chord.sharp = self.readBool()
intonation = 'sharp' if chord.sharp else 'flat'
self.skip(3)
chord.root = gp.PitchClass(self.readByte(), intonation=intonation)
chord.type = gp.ChordType(self.readByte())
chord.extension = gp.ChordExtension(self.readByte())
chord.bass = gp.PitchClass(self.readInt(), intonation=intonation)
chord.tonality = gp.ChordAlteration(self.readInt())
chord.add = self.readBool()
chord.name = self.readByteSizeString(22)
chord.fifth = gp.ChordAlteration(self.readByte())
chord.ninth = gp.ChordAlteration(self.readByte())
chord.eleventh = gp.ChordAlteration(self.readByte())
chord.firstFret = self.readInt()
for i in range(7):
fret = self.readInt()
if i < len(chord.strings):
chord.strings[i] = fret
chord.barres = []
barresCount = self.readByte()
barreFrets = self.readByte(5)
barreStarts = self.readByte(5)
barreEnds = self.readByte(5)
for fret, start, end, _ in zip(barreFrets, barreStarts, barreEnds, range(barresCount)):
barre = gp.Barre(fret, start, end)
chord.barres.append(barre)
chord.omissions = self.readBool(7)
self.skip(1)
chord.fingerings = list(map(gp.Fingering, self.readSignedByte(7)))
chord.show = self.readBool()
[docs] def readBeatEffects(self, noteEffect):
"""Read beat effects.
Beat effects are read using two byte flags.
The first byte of flags is:
- *0x01*: *blank*
- *0x02*: wide vibrato
- *0x04*: *blank*
- *0x08*: *blank*
- *0x10*: fade in
- *0x20*: slap effect
- *0x40*: beat stroke
- *0x80*: *blank*
The second byte of flags is:
- *0x01*: rasgueado
- *0x02*: pick stroke
- *0x04*: tremolo bar
- *0x08*: *blank*
- *0x10*: *blank*
- *0x20*: *blank*
- *0x40*: *blank*
- *0x80*: *blank*
Flags are followed by:
- Slap effect: :ref:`signed-byte`. For value mapping see
:class:`guitarpro.models.SlapEffect`.
- Tremolo bar. See :meth:`readTremoloBar`.
- Beat stroke. See :meth:`readBeatStroke`.
- Pick stroke: :ref:`signed-byte`. For value mapping see
:class:`guitarpro.models.BeatStrokeDirection`.
"""
beatEffect = gp.BeatEffect()
flags1 = self.readSignedByte()
flags2 = self.readSignedByte()
beatEffect.vibrato = bool(flags1 & 0x02) or beatEffect.vibrato
beatEffect.fadeIn = bool(flags1 & 0x10)
if flags1 & 0x20:
value = self.readSignedByte()
beatEffect.slapEffect = gp.SlapEffect(value)
if flags2 & 0x04:
beatEffect.tremoloBar = self.readTremoloBar()
if flags1 & 0x40:
beatEffect.stroke = self.readBeatStroke()
beatEffect.hasRasgueado = bool(flags2 & 0x01)
if flags2 & 0x02:
direction = self.readSignedByte()
beatEffect.pickStroke = gp.BeatStrokeDirection(direction)
return beatEffect
[docs] def readTremoloBar(self):
return self.readBend()
[docs] def readMixTableChange(self, measure):
"""Read mix table change.
Mix table change in Guitar Pro 4 format extends Guitar Pro 3
format. It constists of :meth:`values
<guitarpro.gp3.GP3File.readMixTableChangeValues>`,
:meth:`durations
<guitarpro.gp3.GP3File.readMixTableChangeDurations>`, and, new
to GP3, :meth:`flags <readMixTableChangeFlags>`.
"""
tableChange = super().readMixTableChange(measure)
self.readMixTableChangeFlags(tableChange)
return tableChange
[docs] def readMixTableChangeFlags(self, tableChange):
"""Read mix table change flags.
The meaning of flags:
- *0x01*: change volume for all tracks
- *0x02*: change balance for all tracks
- *0x04*: change chorus for all tracks
- *0x08*: change reverb for all tracks
- *0x10*: change phaser for all tracks
- *0x20*: change tremolo for all tracks
"""
flags = self.readSignedByte()
if tableChange.volume is not None:
tableChange.volume.allTracks = bool(flags & 0x01)
if tableChange.balance is not None:
tableChange.balance.allTracks = bool(flags & 0x02)
if tableChange.chorus is not None:
tableChange.chorus.allTracks = bool(flags & 0x04)
if tableChange.reverb is not None:
tableChange.reverb.allTracks = bool(flags & 0x08)
if tableChange.phaser is not None:
tableChange.phaser.allTracks = bool(flags & 0x10)
if tableChange.tremolo is not None:
tableChange.tremolo.allTracks = bool(flags & 0x20)
return flags
[docs] def readNoteEffects(self, note):
"""Read note effects.
The effects presence for the current note is set by the 2 bytes
of flags.
First set of flags:
- *0x01*: bend
- *0x02*: hammer-on/pull-off
- *0x04*: *blank*
- *0x08*: let-ring
- *0x10*: grace note
- *0x20*: *blank*
- *0x40*: *blank*
- *0x80*: *blank*
Second set of flags:
- *0x01*: staccato
- *0x02*: palm mute
- *0x04*: tremolo picking
- *0x08*: slide
- *0x10*: harmonic
- *0x20*: trill
- *0x40*: vibrato
- *0x80*: *blank*
Flags are followed by:
- Bend. See :meth:`readBend`.
- Grace note. See :meth:`readGrace`.
- Tremolo picking. See :meth:`readTremoloPicking`.
- Slide. See :meth:`readSlides`.
- Harmonic. See :meth:`readHarmonic`.
- Trill. See :meth:`readTrill`.
"""
noteEffect = note.effect or gp.NoteEffect()
flags1 = self.readSignedByte()
flags2 = self.readSignedByte()
noteEffect.hammer = bool(flags1 & 0x02)
noteEffect.letRing = bool(flags1 & 0x08)
noteEffect.staccato = bool(flags2 & 0x01)
noteEffect.palmMute = bool(flags2 & 0x02)
noteEffect.vibrato = bool(flags2 & 0x40) or noteEffect.vibrato
if flags1 & 0x01:
noteEffect.bend = self.readBend()
if flags1 & 0x10:
noteEffect.grace = self.readGrace()
if flags2 & 0x04:
noteEffect.tremoloPicking = self.readTremoloPicking()
if flags2 & 0x08:
noteEffect.slides = self.readSlides()
if flags2 & 0x10:
noteEffect.harmonic = self.readHarmonic(note)
if flags2 & 0x20:
noteEffect.trill = self.readTrill()
return noteEffect
[docs] def readTremoloPicking(self):
"""Read tremolo picking.
Tremolo constists of picking speed encoded in
:ref:`signed-byte`. For value mapping refer to
:meth:`fromTremoloValue`.
"""
value = self.readSignedByte()
tp = gp.TremoloPickingEffect()
tp.duration.value = self.fromTremoloValue(value)
return tp
[docs] def fromTremoloValue(self, value):
"""Convert tremolo picking speed to actual duration.
Values are:
- *1*: eighth
- *2*: sixteenth
- *3*: thirtySecond
"""
if value == 1:
return gp.Duration.eighth
elif value == 2:
return gp.Duration.sixteenth
elif value == 3:
return gp.Duration.thirtySecond
[docs] def readSlides(self):
"""Read slides.
Slide is encoded in :ref:`signed-byte`. See
:class:`guitarpro.models.SlideType` for value mapping.
"""
return [gp.SlideType(self.readSignedByte())]
[docs] def readHarmonic(self, note):
"""Read harmonic.
Harmonic is encoded in :ref:`signed-byte`. Values correspond to:
- *1*: natural harmonic
- *3*: tapped harmonic
- *4*: pinch harmonic
- *5*: semi-harmonic
- *15*: artificial harmonic on (*n + 5*)th fret
- *17*: artificial harmonic on (*n + 7*)th fret
- *22*: artificial harmonic on (*n + 12*)th fret
"""
harmonicType = self.readSignedByte()
if harmonicType == 1:
harmonic = gp.NaturalHarmonic()
elif harmonicType == 3:
harmonic = gp.TappedHarmonic()
elif harmonicType == 4:
harmonic = gp.PinchHarmonic()
elif harmonicType == 5:
harmonic = gp.SemiHarmonic()
elif harmonicType == 15:
pitch = gp.PitchClass((note.realValue + 7) % 12)
octave = gp.Octave.ottava
harmonic = gp.ArtificialHarmonic(pitch, octave)
elif harmonicType == 17:
pitch = gp.PitchClass(note.realValue)
octave = gp.Octave.quindicesima
harmonic = gp.ArtificialHarmonic(pitch, octave)
elif harmonicType == 22:
pitch = gp.PitchClass(note.realValue)
octave = gp.Octave.ottava
harmonic = gp.ArtificialHarmonic(pitch, octave)
return harmonic
[docs] def readTrill(self):
"""Read trill.
- Fret: :ref:`signed-byte`.
- Period: :ref:`signed-byte`. See :meth:`fromTrillPeriod`.
"""
trill = gp.TrillEffect()
trill.fret = self.readSignedByte()
trill.duration.value = self.fromTrillPeriod(self.readSignedByte())
return trill
[docs] def fromTrillPeriod(self, period):
"""Convert trill period to actual duration.
Values are:
- *1*: sixteenth
- *2*: thirty-second
- *3*: sixty-fourth
"""
if period == 1:
return gp.Duration.sixteenth
elif period == 2:
return gp.Duration.thirtySecond
elif period == 3:
return gp.Duration.sixtyFourth
# Writing
# =======
def writeSong(self, song):
self.writeVersion()
self.writeClipboard(song.clipboard)
self.writeInfo(song)
self._tripletFeel = song.tracks[0].measures[0].tripletFeel.value
self.writeBool(self._tripletFeel)
self.writeLyrics(song.lyrics)
self.writeInt(song.tempo)
self.writeInt(song.key.value[0])
self.writeSignedByte(0) # octave
self.writeMidiChannels(song.tracks)
measureCount = len(song.tracks[0].measures)
trackCount = len(song.tracks)
self.writeInt(measureCount)
self.writeInt(trackCount)
with self.annotateErrors('writing'):
self.writeMeasureHeaders(song.tracks[0].measures)
self.writeTracks(song.tracks)
self.writeMeasures(song.tracks)
def writeClipboard(self, clipboard):
if clipboard is None:
return
self.writeInt(clipboard.startMeasure)
self.writeInt(clipboard.stopMeasure)
self.writeInt(clipboard.startTrack)
self.writeInt(clipboard.stopTrack)
def writeLyrics(self, lyrics):
self.writeInt(lyrics.trackChoice)
for line in lyrics.lines:
self.writeInt(line.startingMeasure)
self.writeIntSizeString(line.lyrics)
def writeBeat(self, beat):
flags = 0x00
if beat.duration.isDotted:
flags |= 0x01
if beat.effect.isChord:
flags |= 0x02
if beat.text is not None:
flags |= 0x04
if not beat.effect.isDefault:
flags |= 0x08
if beat.effect.mixTableChange is not None:
if not beat.effect.mixTableChange.isJustWah or self.versionTuple[0] > 4:
flags |= 0x10
if beat.duration.tuplet != gp.Tuplet():
flags |= 0x20
if beat.status != gp.BeatStatus.normal:
flags |= 0x40
self.writeSignedByte(flags)
if flags & 0x40:
self.writeByte(beat.status.value)
self.writeDuration(beat.duration, flags)
if flags & 0x02:
self.writeChord(beat.effect.chord)
if flags & 0x04:
self.writeIntByteSizeString(beat.text)
if flags & 0x08:
self.writeBeatEffects(beat)
if flags & 0x10:
self.writeMixTableChange(beat.effect.mixTableChange)
self.writeNotes(beat)
def writeChord(self, chord):
self.writeSignedByte(1) # signify GP4 chord format
self.writeBool(chord.sharp)
self.placeholder(3)
self.writeByte(chord.root.value if chord.root else 0)
self.writeByte(self.getEnumValue(chord.type) if chord.type else 0)
self.writeByte(self.getEnumValue(chord.extension) if chord.extension else 0)
self.writeInt(chord.bass.value if chord.bass else 0)
self.writeInt(chord.tonality.value if chord.tonality else 0)
self.writeBool(chord.add)
self.writeByteSizeString(chord.name, 22)
self.writeByte(chord.fifth.value if chord.fifth else 0)
self.writeByte(chord.ninth.value if chord.ninth else 0)
self.writeByte(chord.eleventh.value if chord.eleventh else 0)
self.writeInt(chord.firstFret)
for fret in clamp(chord.strings, 7, fillvalue=-1):
self.writeInt(fret)
self.writeByte(len(chord.barres))
if chord.barres:
barreFrets, barreStarts, barreEnds = zip(*map(attr.astuple, chord.barres))
else:
barreFrets, barreStarts, barreEnds = [], [], []
for fret in clamp(barreFrets, 5, fillvalue=0):
self.writeByte(fret)
for start in clamp(barreStarts, 5, fillvalue=0):
self.writeByte(start)
for end in clamp(barreEnds, 5, fillvalue=0):
self.writeByte(end)
for omission in clamp(chord.omissions, 7, fillvalue=True):
self.writeBool(omission)
self.placeholder(1)
placeholder = gp.Fingering(-2)
for fingering in clamp(chord.fingerings, 7, fillvalue=placeholder):
self.writeSignedByte(fingering.value)
self.writeBool(chord.show)
def writeBeatEffects(self, beat):
flags1 = 0x00
if beat.effect.vibrato:
flags1 |= 0x02
if beat.effect.fadeIn:
flags1 |= 0x10
if beat.effect.isSlapEffect:
flags1 |= 0x20
if beat.effect.stroke != gp.BeatStroke():
flags1 |= 0x40
self.writeSignedByte(flags1)
flags2 = 0x00
if beat.effect.hasRasgueado:
flags2 |= 0x01
if beat.effect.hasPickStroke:
flags2 |= 0x02
if beat.effect.isTremoloBar:
flags2 |= 0x04
self.writeSignedByte(flags2)
if flags1 & 0x20:
self.writeSignedByte(beat.effect.slapEffect.value)
if flags2 & 0x04:
self.writeTremoloBar(beat.effect.tremoloBar)
if flags1 & 0x40:
self.writeBeatStroke(beat.effect.stroke)
if flags2 & 0x02:
self.writeSignedByte(beat.effect.pickStroke.value)
def writeTremoloBar(self, tremoloBar):
self.writeBend(tremoloBar)
def writeMixTableChange(self, tableChange):
super().writeMixTableChange(tableChange)
self.writeMixTableChangeFlags(tableChange)
def writeMixTableChangeFlags(self, tableChange):
flags = 0x00
if tableChange.volume is not None and tableChange.volume.allTracks:
flags |= 0x01
if tableChange.balance is not None and tableChange.balance.allTracks:
flags |= 0x02
if tableChange.chorus is not None and tableChange.chorus.allTracks:
flags |= 0x04
if tableChange.reverb is not None and tableChange.reverb.allTracks:
flags |= 0x08
if tableChange.phaser is not None and tableChange.phaser.allTracks:
flags |= 0x10
if tableChange.tremolo is not None and tableChange.tremolo.allTracks:
flags |= 0x20
self.writeSignedByte(flags)
def writeNote(self, note):
flags = self.packNoteFlags(note)
self.writeByte(flags)
if flags & 0x20:
self.writeByte(self.getEnumValue(note.type))
if flags & 0x01:
self.writeSignedByte(note.duration)
self.writeSignedByte(note.tuplet)
if flags & 0x10:
value = self.packVelocity(note.velocity)
self.writeSignedByte(value)
if flags & 0x20:
fret = note.value if note.type != gp.NoteType.tie else 0
self.writeSignedByte(fret)
if flags & 0x80:
self.writeSignedByte(self.getEnumValue(note.effect.leftHandFinger))
self.writeSignedByte(self.getEnumValue(note.effect.rightHandFinger))
if flags & 0x08:
self.writeNoteEffects(note)
def packNoteFlags(self, note):
flags = super().packNoteFlags(note)
if note.effect.accentuatedNote:
flags |= 0x40
if note.effect.isFingering:
flags |= 0x80
return flags
def writeNoteEffects(self, note):
noteEffect = note.effect
flags1 = 0x00
if noteEffect.isBend:
flags1 |= 0x01
if noteEffect.hammer:
flags1 |= 0x02
if noteEffect.letRing:
flags1 |= 0x08
if noteEffect.isGrace:
flags1 |= 0x10
self.writeSignedByte(flags1)
flags2 = 0x00
if noteEffect.staccato:
flags2 |= 0x01
if noteEffect.palmMute:
flags2 |= 0x02
if noteEffect.isTremoloPicking:
flags2 |= 0x04
if noteEffect.slides:
flags2 |= 0x08
if noteEffect.isHarmonic:
flags2 |= 0x10
if noteEffect.isTrill:
flags2 |= 0x20
if noteEffect.vibrato:
flags2 |= 0x40
self.writeSignedByte(flags2)
if flags1 & 0x01:
self.writeBend(noteEffect.bend)
if flags1 & 0x10:
self.writeGrace(noteEffect.grace)
if flags2 & 0x04:
self.writeTremoloPicking(noteEffect.tremoloPicking)
if flags2 & 0x08:
self.writeSlides(noteEffect.slides)
if flags2 & 0x10:
self.writeHarmonic(note, noteEffect.harmonic)
if flags2 & 0x20:
self.writeTrill(noteEffect.trill)
def writeTremoloPicking(self, tremoloPicking):
self.writeSignedByte(self.toTremoloValue(tremoloPicking.duration.value))
def writeSlides(self, slides):
self.writeSignedByte(slides[0].value)
def toTremoloValue(self, value):
if value == gp.Duration.eighth:
return 1
elif value == gp.Duration.sixteenth:
return 2
elif value == gp.Duration.thirtySecond:
return 3
def writeHarmonic(self, note, harmonic):
if not isinstance(harmonic, gp.ArtificialHarmonic):
byte = harmonic.type
else:
if harmonic.pitch and harmonic.octave:
if harmonic.pitch.value == (note.realValue + 7) % 12 and harmonic.octave == gp.Octave.ottava:
byte = 15
elif harmonic.pitch.value == note.realValue % 12 and harmonic.octave == gp.Octave.quindicesima:
byte = 17
elif harmonic.pitch.value == note.realValue % 12 and harmonic.octave == gp.Octave.ottava:
byte = 22
else:
byte = 22
else:
byte = 22
self.writeSignedByte(byte)
def writeTrill(self, trill):
self.writeSignedByte(trill.fret)
self.writeSignedByte(self.toTrillPeriod(trill.duration.value))
def toTrillPeriod(self, value):
if value == gp.Duration.sixteenth:
return 1
if value == gp.Duration.thirtySecond:
return 2
if value == gp.Duration.sixtyFourth:
return 3