from __future__ import division
import attr
from . import models as gp
from .iobase import GPFileBase
from .utils import clamp, bit_length
[docs]class GP3File(GPFileBase):
"""A reader for GuitarPro 3 files."""
_tripletFeel = gp.TripletFeel.none
# Reading
# =======
[docs] def readSong(self):
"""Read the song.
A song consists of score information, triplet feel, 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.
- Tempo: :ref:`int`.
- Key: :ref:`int`. Key signature of the song.
- 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
self.readInfo(song)
self._tripletFeel = gp.TripletFeel.eighth if self.readBool() else gp.TripletFeel.none
song.tempo = self.readInt()
song.key = gp.KeySignature((self.readInt(), 0))
channels = self.readMidiChannels()
measureCount = self.readInt()
trackCount = self.readInt()
self.readMeasureHeaders(song, measureCount)
self.readTracks(song, trackCount, channels)
self.readMeasures(song)
return song
[docs] def readInfo(self, song):
"""Read score information.
Score information consists of sequence of
:ref:`IntByteSizeStrings <int-byte-size-string>`:
- title
- subtitle
- artist
- album
- words
- copyright
- tabbed by
- instructions
The sequence if followed by notice. Notice starts with the
number of notice lines stored in :ref:`int`. Each line is
encoded in :ref:`int-byte-size-string`.
"""
song.title = self.readIntByteSizeString()
song.subtitle = self.readIntByteSizeString()
song.artist = self.readIntByteSizeString()
song.album = self.readIntByteSizeString()
song.words = self.readIntByteSizeString()
song.music = song.words
song.copyright = self.readIntByteSizeString()
song.tab = self.readIntByteSizeString()
song.instructions = self.readIntByteSizeString()
notesCount = self.readInt()
song.notice = []
for _ in range(notesCount):
song.notice.append(self.readIntByteSizeString())
[docs] def readMidiChannels(self):
"""Read MIDI channels.
Guitar Pro format provides 64 channels (4 MIDI ports by 16
channels), the channels are stored in this order:
- port1/channel1
- port1/channel2
- ...
- port1/channel16
- port2/channel1
- ...
- port4/channel16
Each channel has the following form:
- Instrument: :ref:`int`.
- Volume: :ref:`byte`.
- Balance: :ref:`byte`.
- Chorus: :ref:`byte`.
- Reverb: :ref:`byte`.
- Phaser: :ref:`byte`.
- Tremolo: :ref:`byte`.
- blank1: :ref:`byte`.
- blank2: :ref:`byte`.
"""
channels = []
for i in range(64):
newChannel = gp.MidiChannel()
newChannel.channel = i
newChannel.effectChannel = i
instrument = self.readInt()
if newChannel.isPercussionChannel and instrument == -1:
instrument = 0
newChannel.instrument = instrument
newChannel.volume = self.toChannelShort(self.readSignedByte())
newChannel.balance = self.toChannelShort(self.readSignedByte())
newChannel.chorus = self.toChannelShort(self.readSignedByte())
newChannel.reverb = self.toChannelShort(self.readSignedByte())
newChannel.phaser = self.toChannelShort(self.readSignedByte())
newChannel.tremolo = self.toChannelShort(self.readSignedByte())
channels.append(newChannel)
# Backward compatibility with version 3.0
self.skip(2)
return channels
def toChannelShort(self, data):
value = max(-32768, min(32767, (data << 3) - 1))
return max(value, -1) + 1
def readRepeatAlternative(self, measureHeaders):
value = self.readByte()
existingAlternatives = 0
for header in reversed(measureHeaders):
if header.isRepeatOpen:
break
existingAlternatives |= header.repeatAlternative
return (1 << value) - 1 ^ existingAlternatives
[docs] def readMarker(self, header):
"""Read marker.
The markers are written in two steps. First is written an
integer equal to the marker's name length + 1, then a string
containing the marker's name. Finally the marker's color is
written.
"""
marker = gp.Marker()
marker.title = self.readIntByteSizeString()
marker.color = self.readColor()
return marker
[docs] def readColor(self):
"""Read color.
Colors are used by :class:`guitarpro.models.Marker` and
:class:`guitarpro.models.Track`. They consist of 3 consecutive
bytes and one blank byte.
"""
r = self.readByte()
g = self.readByte()
b = self.readByte()
self.skip(1)
return gp.Color(r, g, b)
[docs] def readTracks(self, song, trackCount, channels):
"""Read tracks.
The tracks are written one after another, their number having
been specified previously in :meth:`GP3File.readSong`.
:param trackCount: number of tracks to expect.
"""
for i in range(trackCount):
track = gp.Track(song, i + 1, strings=[], measures=[])
self.readTrack(track, channels)
song.tracks.append(track)
[docs] def readTrack(self, track, channels):
"""Read track.
The first byte is the track's flags. It presides the track's
attributes:
- *0x01*: drums track
- *0x02*: 12 stringed guitar track
- *0x04*: banjo track
- *0x08*: *blank*
- *0x10*: *blank*
- *0x20*: *blank*
- *0x40*: *blank*
- *0x80*: *blank*
Flags are followed by:
- Name: :ref:`byte-size-string`. A 40 characters long string
containing the track's name.
- Number of strings: :ref:`int`. An integer equal to the number
of strings of the track.
- Tuning of the strings: List of 7 :ref:`Ints <int>`. The tuning
of the strings is stored as a 7-integers table, the "Number of
strings" first integers being really used. The strings are
stored from the highest to the lowest.
- Port: :ref:`int`. The number of the MIDI port used.
- Channel. See :meth:`GP3File.readChannel`.
- Number of frets: :ref:`int`. The number of frets of the
instrument.
- Height of the capo: :ref:`int`. The number of the fret on
which a capo is set. If no capo is used, the value is 0.
- Track's color. The track's displayed color in Guitar Pro.
"""
flags = self.readByte()
track.isPercussionTrack = bool(flags & 0x01)
track.is12StringedGuitarTrack = bool(flags & 0x02)
track.isBanjoTrack = bool(flags & 0x04)
track.name = self.readByteSizeString(40)
stringCount = self.readInt()
for i in range(7):
iTuning = self.readInt()
if stringCount > i:
oString = gp.GuitarString(i + 1, iTuning)
track.strings.append(oString)
track.port = self.readInt()
track.channel = self.readChannel(channels)
if track.channel.channel == 9:
track.isPercussionTrack = True
track.fretCount = self.readInt()
track.offset = self.readInt()
track.color = self.readColor()
[docs] def readChannel(self, channels):
"""Read MIDI channel.
MIDI channel in Guitar Pro is represented by two integers. First
is zero-based number of channel, second is zero-based number of
channel used for effects.
"""
index = self.readInt() - 1
effectChannel = self.readInt() - 1
if 0 <= index < len(channels):
trackChannel = channels[index]
if trackChannel.instrument < 0:
trackChannel.instrument = 0
if not trackChannel.isPercussionChannel:
trackChannel.effectChannel = effectChannel
return trackChannel
[docs] def readMeasures(self, song):
"""Read measures.
Measures are written in the following order:
- measure 1/track 1
- measure 1/track 2
- ...
- measure 1/track m
- measure 2/track 1
- measure 2/track 2
- ...
- measure 2/track m
- ...
- measure n/track 1
- measure n/track 2
- ...
- measure n/track m
"""
tempo = gp.Tempo(song.tempo)
start = gp.Duration.quarterTime
for header in song.measureHeaders:
header.start = start
for track in song.tracks:
measure = gp.Measure(track, header)
tempo = header.tempo
track.measures.append(measure)
self.readMeasure(measure)
header.tempo = tempo
start += header.length
[docs] def readMeasure(self, measure):
"""Read measure.
The measure is written as number of beats followed by sequence
of beats.
"""
start = measure.start
voice = measure.voices[0]
self.readVoice(start, voice)
def readVoice(self, start, voice):
beats = self.readInt()
for beat in range(beats):
start += self.readBeat(start, voice)
[docs] def readBeat(self, start, voice):
"""Read beat.
The first byte is the beat flags. It lists the data present in
the current beat:
- *0x01*: dotted notes
- *0x02*: presence of a chord diagram
- *0x04*: presence of a text
- *0x08*: presence of effects
- *0x10*: presence of a mix table change event
- *0x20*: the beat is a n-tuplet
- *0x40*: status: True if the beat is empty of if it is a rest
- *0x80*: *blank*
Flags are followed by:
- Status: :ref:`byte`. If flag at *0x40* is true, read one byte.
If value of the byte is *0x00* then beat is empty, if value is
*0x02* then the beat is rest.
- Beat duration: :ref:`byte`. See :meth:`readDuration`.
- Chord diagram. See :meth:`readChord`.
- Text. See :meth:`readText`.
- Beat effects. See :meth:`readBeatEffects`.
- Mix table change effect. See :meth:`readMixTableChange`.
"""
flags = self.readByte()
beat = self.getBeat(voice, start)
if flags & 0x40:
beat.status = gp.BeatStatus(self.readByte())
else:
beat.status = gp.BeatStatus.normal
duration = self.readDuration(flags)
effect = gp.NoteEffect()
if flags & 0x02:
beat.effect.chord = self.readChord(len(voice.measure.track.strings))
if flags & 0x04:
beat.text = self.readText()
if flags & 0x08:
chord = beat.effect.chord
beat.effect = self.readBeatEffects(effect)
beat.effect.chord = chord
if flags & 0x10:
mixTableChange = self.readMixTableChange(voice.measure)
beat.effect.mixTableChange = mixTableChange
self.readNotes(voice.measure.track, beat, duration, effect)
return duration.time if not beat.status == gp.BeatStatus.empty else 0
[docs] def getBeat(self, voice, start):
"""Get beat from measure by start time."""
for beat in reversed(voice.beats):
if beat.start == start:
return beat
newBeat = gp.Beat(voice)
newBeat.start = start
voice.beats.append(newBeat)
return newBeat
[docs] def readDuration(self, flags):
"""Read beat duration.
Duration is composed of byte signifying duration and an integer
that maps to :class:`guitarpro.models.Tuplet`.
The byte maps to following values:
- *-2*: whole note
- *-1*: half note
- *0*: quarter note
- *1*: eighth note
- *2*: sixteenth note
- *3*: thirty-second note
If flag at *0x20* is true, the tuplet is read.
"""
duration = gp.Duration()
duration.value = 1 << (self.readSignedByte() + 2)
duration.isDotted = bool(flags & 0x01)
if flags & 0x20:
iTuplet = self.readInt()
if iTuplet == 3:
duration.tuplet.enters = 3
duration.tuplet.times = 2
elif iTuplet == 5:
duration.tuplet.enters = 5
duration.tuplet.times = 4
elif iTuplet == 6:
duration.tuplet.enters = 6
duration.tuplet.times = 4
elif iTuplet == 7:
duration.tuplet.enters = 7
duration.tuplet.times = 4
elif iTuplet == 9:
duration.tuplet.enters = 9
duration.tuplet.times = 8
elif iTuplet == 10:
duration.tuplet.enters = 10
duration.tuplet.times = 8
elif iTuplet == 11:
duration.tuplet.enters = 11
duration.tuplet.times = 8
elif iTuplet == 12:
duration.tuplet.enters = 12
duration.tuplet.times = 8
elif iTuplet == 13:
duration.tuplet.enters = 13
duration.tuplet.times = 8
return duration
[docs] def readChord(self, stringCount):
"""Read chord diagram.
First byte is chord header. If it's set to 0, then following
chord is written in default (GP3) format. If chord header is set
to 1, then chord diagram in encoded in more advanced (GP4)
format.
"""
chord = gp.Chord(stringCount)
chord.newFormat = self.readBool()
if not chord.newFormat:
self.readOldChord(chord)
else:
self.readNewChord(chord)
return chord
[docs] def readOldChord(self, chord):
"""Read chord diagram encoded in GP3 format.
Chord diagram is read as follows:
- Name: :ref:`int-byte-size-string`. Name of the chord, e.g.
*Em*.
- First fret: :ref:`int`. The fret from which the chord is
displayed in chord editor.
- List of frets: 6 :ref:`Ints <int>`. Frets are listed in order:
fret on the string 1, fret on the string 2, ..., fret on the
string 6. If string is untouched then the values of fret is
*-1*.
"""
chord.name = self.readIntByteSizeString()
chord.firstFret = self.readInt()
if chord.firstFret:
for i in range(6):
fret = self.readInt()
if i < len(chord.strings):
chord.strings[i] = fret
[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:`int`. Values are:
* -1 for customized chords
* 0: C
* 1: C#
* ...
- Type: :ref:`int`. Determines the chord type as followed. See
:class:`guitarpro.models.ChordType` for mapping.
- Chord extension: :ref:`int`. 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 alteration: :ref:`int`. Maps to
:class:`guitarpro.models.ChordAlteration`.
- Ninth alteration: :ref:`int`. Maps to
:class:`guitarpro.models.ChordAlteration`.
- Eleventh alteration: :ref:`int`. Maps to
:class:`guitarpro.models.ChordAlteration`.
- List of frets: 6 :ref:`Ints <int>`. Fret values are saved as
in default format.
- Count of barres: :ref:`int`. Maximum count is 2.
- Barre frets: 2 :ref:`Ints <int>`.
- Barre start strings: 2 :ref:`Ints <int>`.
- Barre end string: 2 :ref:`Ints <int>`.
- Omissions: 7 :ref:`Bools <bool>`. If the value is true then
note is played in chord.
- Blank space, 1 :ref:`byte`.
"""
chord.sharp = self.readBool()
intonation = 'sharp' if chord.sharp else 'flat'
self.skip(3)
chord.root = gp.PitchClass(self.readInt(), intonation=intonation)
chord.type = gp.ChordType(self.readInt())
chord.extension = gp.ChordExtension(self.readInt())
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.readInt())
chord.ninth = gp.ChordAlteration(self.readInt())
chord.eleventh = gp.ChordAlteration(self.readInt())
chord.firstFret = self.readInt()
for i in range(6):
fret = self.readInt()
if i < len(chord.strings):
chord.strings[i] = fret
chord.barres = []
barresCount = self.readInt()
barreFrets = self.readInt(2)
barreStarts = self.readInt(2)
barreEnds = self.readInt(2)
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)
[docs] def readText(self):
"""Read beat text.
Text is stored in :ref:`int-byte-size-string`.
"""
text = gp.BeatText()
text.value = self.readIntByteSizeString()
return text
[docs] def readBeatEffects(self, effect):
"""Read beat effects.
The first byte is effects flags:
- *0x01*: vibrato
- *0x02*: wide vibrato
- *0x04*: natural harmonic
- *0x08*: artificial harmonic
- *0x10*: fade in
- *0x20*: tremolo bar or slap effect
- *0x40*: beat stroke direction
- *0x80*: *blank*
- Tremolo bar or slap effect: :ref:`byte`. If it's 0 then
tremolo bar should be read (see :meth:`readTremoloBar`). Else
it's tapping and values of the byte map to:
- *1*: tap
- *2*: slap
- *3*: pop
- Beat stroke direction. See :meth:`readBeatStroke`.
"""
beatEffects = gp.BeatEffect()
flags1 = self.readByte()
effect.vibrato = bool(flags1 & 0x01) or effect.vibrato
beatEffects.vibrato = bool(flags1 & 0x02) or beatEffects.vibrato
beatEffects.fadeIn = bool(flags1 & 0x10)
if flags1 & 0x20:
flags2 = self.readByte()
beatEffects.slapEffect = gp.SlapEffect(flags2)
if beatEffects.slapEffect == gp.SlapEffect.none:
beatEffects.tremoloBar = self.readTremoloBar()
else:
self.readInt()
if flags1 & 0x40:
beatEffects.stroke = self.readBeatStroke()
if flags1 & 0x04:
effect.harmonic = gp.NaturalHarmonic()
if flags1 & 0x08:
effect.harmonic = gp.ArtificialHarmonic()
return beatEffects
[docs] def readTremoloBar(self):
"""Read tremolo bar beat effect.
The only type of tremolo bar effect Guitar Pro 3 supports is
:attr:`dip <guitarpro.models.BendType.dip>`. The value of the
effect is encoded in :ref:`Int` and shows how deep tremolo bar
is pressed.
"""
barEffect = gp.BendEffect()
barEffect.type = gp.BendType.dip
barEffect.value = self.readInt()
barEffect.points = [
gp.BendPoint(0, 0),
gp.BendPoint(round(gp.BendEffect.maxPosition / 2),
round(-barEffect.value / self.bendSemitone)),
gp.BendPoint(gp.BendEffect.maxPosition, 0),
]
return barEffect
[docs] def readBeatStroke(self):
"""Read beat stroke.
Beat stroke consists of two :ref:`Bytes <byte>` which correspond
to stroke up and stroke down speed. See
:class:`guitarpro.models.BeatStrokeDirection` for value mapping.
"""
strokeDown = self.readSignedByte()
strokeUp = self.readSignedByte()
if strokeUp > 0:
return gp.BeatStroke(gp.BeatStrokeDirection.up, self.toStrokeValue(strokeUp))
elif strokeDown > 0:
return gp.BeatStroke(gp.BeatStrokeDirection.down, self.toStrokeValue(strokeDown))
[docs] def toStrokeValue(self, value):
"""Unpack stroke value.
Stroke value maps to:
- *1*: hundred twenty-eighth
- *2*: sixty-fourth
- *3*: thirty-second
- *4*: sixteenth
- *5*: eighth
- *6*: quarter
"""
if value == 1:
return gp.Duration.hundredTwentyEighth
elif value == 2:
return gp.Duration.sixtyFourth
elif value == 3:
return gp.Duration.thirtySecond
elif value == 4:
return gp.Duration.sixteenth
elif value == 5:
return gp.Duration.eighth
elif value == 6:
return gp.Duration.quarter
else:
return gp.Duration.sixtyFourth
[docs] def readMixTableChange(self, measure):
"""Read mix table change.
List of values is read first. See
:meth:`readMixTableChangeValues`.
List of values is followed by the list of durations for
parameters that have changed. See
:meth:`readMixTableChangeDurations`.
"""
tableChange = gp.MixTableChange()
self.readMixTableChangeValues(tableChange, measure)
self.readMixTableChangeDurations(tableChange)
return tableChange
[docs] def readMixTableChangeValues(self, tableChange, measure):
"""Read mix table change values.
Mix table change values consist of 7 :ref:`SignedBytes
<signed-byte>` and an :ref:`int`, which correspond to:
- instrument
- volume
- balance
- chorus
- reverb
- phaser
- tremolo
- tempo
If signed byte is *-1* then corresponding parameter hasn't
changed.
"""
instrument = self.readSignedByte()
volume = self.readSignedByte()
balance = self.readSignedByte()
chorus = self.readSignedByte()
reverb = self.readSignedByte()
phaser = self.readSignedByte()
tremolo = self.readSignedByte()
tempo = self.readInt()
if instrument >= 0:
tableChange.instrument = gp.MixTableItem(instrument)
if volume >= 0:
tableChange.volume = gp.MixTableItem(volume)
if balance >= 0:
tableChange.balance = gp.MixTableItem(balance)
if chorus >= 0:
tableChange.chorus = gp.MixTableItem(chorus)
if reverb >= 0:
tableChange.reverb = gp.MixTableItem(reverb)
if phaser >= 0:
tableChange.phaser = gp.MixTableItem(phaser)
if tremolo >= 0:
tableChange.tremolo = gp.MixTableItem(tremolo)
if tempo >= 0:
tableChange.tempo = gp.MixTableItem(tempo)
measure.tempo.value = tempo
[docs] def readMixTableChangeDurations(self, tableChange):
"""Read mix table change durations.
Durations are read for each non-null
:class:`~guitarpro.models.MixTableItem`. Durations are encoded
in :ref:`signed-byte`.
"""
if tableChange.volume is not None:
tableChange.volume.duration = self.readSignedByte()
if tableChange.balance is not None:
tableChange.balance.duration = self.readSignedByte()
if tableChange.chorus is not None:
tableChange.chorus.duration = self.readSignedByte()
if tableChange.reverb is not None:
tableChange.reverb.duration = self.readSignedByte()
if tableChange.phaser is not None:
tableChange.phaser.duration = self.readSignedByte()
if tableChange.tremolo is not None:
tableChange.tremolo.duration = self.readSignedByte()
if tableChange.tempo is not None:
tableChange.tempo.duration = self.readSignedByte()
tableChange.hideTempo = False
[docs] def readNotes(self, track, beat, duration, effect=None):
"""Read notes.
First byte lists played strings:
- *0x01*: 7th string
- *0x02*: 6th string
- *0x04*: 5th string
- *0x08*: 4th string
- *0x10*: 3th string
- *0x20*: 2th string
- *0x40*: 1th string
- *0x80*: *blank*
"""
stringFlags = self.readByte()
for string in track.strings:
if stringFlags & 1 << (7 - string.number):
note = gp.Note(beat)
beat.notes.append(note)
self.readNote(note, string, track)
beat.duration = duration
[docs] def readNote(self, note, guitarString, track):
"""Read note.
The first byte is note flags:
- *0x01*: time-independent duration
- *0x02*: heavy accentuated note
- *0x04*: ghost note
- *0x08*: presence of note effects
- *0x10*: dynamics
- *0x20*: fret
- *0x40*: accentuated note
- *0x80*: right hand or left hand fingering
Flags are followed by:
- Note type: :ref:`byte`. Note is normal if values is 1, tied if
value is 2, dead if value is 3.
- Time-independent duration: 2 :ref:`SignedBytes <signed-byte>`.
Correspond to duration and tuplet. See :meth:`readDuration`
for reference.
- Note dynamics: :ref:`signed-byte`. See :meth:`unpackVelocity`.
- Fret number: :ref:`signed-byte`. If flag at *0x20* is set then
read fret number.
- Fingering: 2 :ref:`SignedBytes <signed-byte>`. See
:class:`guitarpro.models.Fingering`.
- Note effects. See :meth:`readNoteEffects`.
"""
flags = self.readByte()
note.string = guitarString.number
note.effect.ghostNote = bool(flags & 0x04)
if flags & 0x20:
note.type = gp.NoteType(self.readByte())
if flags & 0x01:
note.duration = self.readSignedByte()
note.tuplet = self.readSignedByte()
if flags & 0x10:
dyn = self.readSignedByte()
note.velocity = self.unpackVelocity(dyn)
if flags & 0x20:
fret = self.readSignedByte()
if note.type == gp.NoteType.tie:
value = self.getTiedNoteValue(guitarString.number, track)
else:
value = fret
note.value = max(0, min(99, value))
if flags & 0x80:
note.effect.leftHandFinger = gp.Fingering(self.readSignedByte())
note.effect.rightHandFinger = gp.Fingering(self.readSignedByte())
if flags & 0x08:
note.effect = self.readNoteEffects(note)
if note.effect.isHarmonic and isinstance(note.effect.harmonic, gp.TappedHarmonic):
note.effect.harmonic.fret = note.value + 12
return note
[docs] def unpackVelocity(self, dyn):
"""Convert Guitar Pro dynamic value to raw MIDI velocity."""
return (gp.Velocities.minVelocity +
gp.Velocities.velocityIncrement * dyn -
gp.Velocities.velocityIncrement)
[docs] def getTiedNoteValue(self, stringIndex, track):
"""Get note value of tied note."""
for measure in reversed(track.measures):
for voice in reversed(measure.voices):
for beat in voice.beats:
if beat.status != gp.BeatStatus.empty:
for note in beat.notes:
if note.string == stringIndex:
return note.value
return -1
[docs] def readNoteEffects(self, note):
"""Read note effects.
First byte is note effects flags:
- *0x01*: bend presence
- *0x02*: hammer-on/pull-off
- *0x04*: slide
- *0x08*: let-ring
- *0x10*: grace note presence
Flags are followed by:
- Bend. See :meth:`readBend`.
- Grace note. See :meth:`readGrace`.
"""
noteEffect = note.effect or gp.NoteEffect()
flags = self.readByte()
noteEffect.hammer = bool(flags & 0x02)
noteEffect.letRing = bool(flags & 0x08)
if flags & 0x01:
noteEffect.bend = self.readBend()
if flags & 0x10:
noteEffect.grace = self.readGrace()
if flags & 0x04:
noteEffect.slides = self.readSlides()
return noteEffect
[docs] def readBend(self):
"""Read bend.
Encoded as:
- Bend type: :ref:`signed-byte`. See
:class:`guitarpro.models.BendType`.
- Bend value: :ref:`int`.
- Number of bend points: :ref:`int`.
- List of points. Each point consists of:
* Position: :ref:`int`. Shows where point is set along
*x*-axis.
* Value: :ref:`int`. Shows where point is set along *y*-axis.
* Vibrato: :ref:`bool`.
"""
bendEffect = gp.BendEffect()
bendEffect.type = gp.BendType(self.readSignedByte())
bendEffect.value = self.readInt()
pointCount = self.readInt()
for _ in range(pointCount):
position = round(self.readInt() * gp.BendEffect.maxPosition / GPFileBase.bendPosition)
value = round(self.readInt() * gp.BendEffect.semitoneLength / GPFileBase.bendSemitone)
vibrato = self.readBool()
bendEffect.points.append(gp.BendPoint(position, value, vibrato))
if pointCount > 0:
return bendEffect
[docs] def readGrace(self):
"""Read grace note effect.
- Fret: :ref:`signed-byte`. Number of fret.
- Dynamic: :ref:`byte`. Dynamic of a grace note, as in
:attr:`guitarpro.models.Note.velocity`.
- Transition: :ref:`byte`. See
:class:`guitarpro.models.GraceEffectTransition`.
- Duration: :ref:`byte`. Values are:
- *1*: Thirty-second note.
- *2*: Twenty-fourth note.
- *3*: Sixteenth note.
"""
grace = gp.GraceEffect()
grace.fret = self.readSignedByte()
grace.velocity = self.unpackVelocity(self.readByte())
grace.duration = 1 << (7 - self.readByte())
grace.isDead = grace.fret == -1
grace.isOnBeat = False
grace.transition = gp.GraceEffectTransition(self.readSignedByte())
return grace
def readSlides(self):
return [gp.SlideType.shiftSlideTo]
# Writing
# =======
def writeSong(self, song):
self.writeVersion()
self.writeInfo(song)
self._tripletFeel = song.tracks[0].measures[0].tripletFeel.value
self.writeBool(self._tripletFeel)
self.writeInt(song.tempo)
self.writeInt(song.key.value[0])
self.writeMidiChannels(song.tracks)
self.writeInt(len(song.tracks[0].measures))
self.writeInt(len(song.tracks))
self.writeMeasureHeaders(song.tracks[0].measures)
self.writeTracks(song.tracks)
self.writeMeasures(song.tracks)
self.writeInt(0)
def writeInfo(self, song):
self.writeIntByteSizeString(song.title)
self.writeIntByteSizeString(song.subtitle)
self.writeIntByteSizeString(song.artist)
self.writeIntByteSizeString(song.album)
self.writeIntByteSizeString(self.packAuthor(song))
self.writeIntByteSizeString(song.copyright)
self.writeIntByteSizeString(song.tab)
self.writeIntByteSizeString(song.instructions)
self.writeInt(len(song.notice))
for line in song.notice:
self.writeIntByteSizeString(line)
def packAuthor(self, song):
if song.words and song.music:
if song.words != song.music:
return song.words + ', ' + song.music
else:
return song.words
else:
return song.words + song.music
def writeMidiChannels(self, tracks):
def getTrackChannelByChannel(channel):
for track in tracks:
if channel in (track.channel.channel, track.channel.effectChannel):
return track.channel
default = gp.MidiChannel()
default.channel = channel
default.effectChannel = channel
if default.isPercussionChannel:
default.instrument = 0
return default
for channel in map(getTrackChannelByChannel, range(64)):
if channel.isPercussionChannel and channel.instrument == 0:
self.writeInt(-1)
else:
self.writeInt(channel.instrument)
self.writeSignedByte(self.fromChannelShort(channel.volume))
self.writeSignedByte(self.fromChannelShort(channel.balance))
self.writeSignedByte(self.fromChannelShort(channel.chorus))
self.writeSignedByte(self.fromChannelShort(channel.reverb))
self.writeSignedByte(self.fromChannelShort(channel.phaser))
self.writeSignedByte(self.fromChannelShort(channel.tremolo))
# Backward compatibility with version 3.0
self.placeholder(2)
def fromChannelShort(self, data):
value = max(-128, min(127, (data >> 3) - 1))
return value + 1
def writeMeasureHeaders(self, measures):
previous = None
for measure in measures:
self.writeMeasureHeader(measure.header, previous)
previous = measure.header
def writeMeasureHeader(self, header, previous=None):
flags = self.packMeasureHeaderFlags(header, previous=previous)
self.writeMeasureHeaderValues(header, flags)
def packMeasureHeaderFlags(self, header, previous=None):
flags = 0x00
if previous is not None:
if header.timeSignature.numerator != previous.timeSignature.numerator:
flags |= 0x01
if header.timeSignature.denominator.value != previous.timeSignature.denominator.value:
flags |= 0x02
else:
flags |= 0x01
flags |= 0x02
if header.isRepeatOpen:
flags |= 0x04
if header.repeatClose > -1:
flags |= 0x08
if header.repeatAlternative:
flags |= 0x10
if header.marker is not None:
flags |= 0x20
return flags
def writeMeasureHeaderValues(self, header, flags):
self.writeByte(flags)
if flags & 0x01:
self.writeSignedByte(header.timeSignature.numerator)
if flags & 0x02:
self.writeSignedByte(header.timeSignature.denominator.value)
if flags & 0x08:
self.writeSignedByte(header.repeatClose)
if flags & 0x10:
self.writeRepeatAlternative(header.repeatAlternative)
if flags & 0x20:
self.writeMarker(header.marker)
def writeRepeatAlternative(self, value):
first_one = False
i = 0
for i in range(bit_length(value) + 1):
if value & 1 << i:
first_one = True
elif first_one:
break
self.writeByte(i)
def writeMarker(self, marker):
self.writeIntByteSizeString(marker.title)
self.writeColor(marker.color)
def writeColor(self, color):
self.writeByte(color.r)
self.writeByte(color.g)
self.writeByte(color.b)
self.placeholder(1)
def writeTracks(self, tracks):
for number, track in enumerate(tracks, 1):
self.writeTrack(track, number)
def writeTrack(self, track, number):
flags = 0x00
if track.isPercussionTrack:
flags |= 0x01
if track.is12StringedGuitarTrack:
flags |= 0x02
if track.isBanjoTrack:
flags |= 0x04
self.writeByte(flags)
self.writeByteSizeString(track.name, 40)
self.writeInt(len(track.strings))
for i in range(7):
if i < len(track.strings):
tuning = track.strings[i].value
else:
tuning = 0
self.writeInt(tuning)
self.writeInt(track.port)
self.writeChannel(track)
self.writeInt(track.fretCount)
self.writeInt(track.offset)
self.writeColor(track.color)
def writeChannel(self, track):
self.writeInt(track.channel.channel + 1)
self.writeInt(track.channel.effectChannel + 1)
def writeMeasures(self, tracks):
partwiseMeasures = [track.measures for track in tracks]
for timewiseMeasures in zip(*partwiseMeasures):
for measure in timewiseMeasures:
self.writeMeasure(measure)
def writeMeasure(self, measure):
voice = measure.voices[0]
self.writeVoice(voice)
def writeVoice(self, voice):
self.writeInt(len(voice.beats))
for beat in voice.beats:
self.writeBeat(beat)
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 or beat.hasVibrato or beat.hasHarmonic:
flags |= 0x08
if beat.effect.mixTableChange is not None and not beat.effect.mixTableChange.isJustWah:
flags |= 0x10
if beat.duration.tuplet != gp.Tuplet():
flags |= 0x20
if beat.status != gp.BeatStatus.normal:
flags |= 0x40
self.writeByte(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.writeText(beat.text)
if flags & 0x08:
self.writeBeatEffects(beat)
if flags & 0x10:
self.writeMixTableChange(beat.effect.mixTableChange)
self.writeNotes(beat)
def writeDuration(self, duration, flags):
value = bit_length(duration.value) - 3
self.writeSignedByte(value)
if flags & 0x20:
if not duration.tuplet.isSupported():
return
iTuplet = duration.tuplet.enters
self.writeInt(iTuplet)
def writeChord(self, chord):
self.writeBool(chord.newFormat)
if chord.newFormat:
self.writeNewChord(chord)
else:
self.writeOldChord(chord)
def writeOldChord(self, chord):
self.writeIntByteSizeString(chord.name)
self.writeInt(chord.firstFret)
for fret in clamp(chord.strings, 6, fillvalue=-1):
self.writeInt(fret)
def writeNewChord(self, chord):
self.writeBool(chord.sharp)
self.placeholder(3)
self.writeInt(chord.root.value if chord.root else 0)
self.writeInt(chord.type.value if chord.type else 0)
self.writeInt(chord.extension.value 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.writeInt(chord.fifth.value if chord.fifth else 0)
self.writeInt(chord.ninth.value if chord.ninth else 0)
self.writeInt(chord.eleventh.value if chord.eleventh else 0)
self.writeInt(chord.firstFret)
for fret in clamp(chord.strings, 6, fillvalue=-1):
self.writeInt(fret)
barres = chord.barres[:2]
self.writeInt(len(barres))
if barres:
barreFrets, barreStarts, barreEnds = zip(*map(attr.astuple, barres))
else:
barreFrets, barreStarts, barreEnds = [], [], []
for fret in clamp(barreFrets, 2, fillvalue=0):
self.writeInt(fret)
for start in clamp(barreStarts, 2, fillvalue=0):
self.writeInt(start)
for end in clamp(barreEnds, 2, fillvalue=0):
self.writeInt(end)
for omission in clamp(chord.omissions, 7, fillvalue=True):
self.writeBool(omission)
self.placeholder(1)
def writeText(self, text):
self.writeIntByteSizeString(text.value)
def writeBeatEffects(self, beat):
flags1 = 0x00
if beat.hasVibrato:
flags1 |= 0x01
if beat.effect.vibrato:
flags1 |= 0x02
if isinstance(beat.hasHarmonic, gp.NaturalHarmonic):
flags1 |= 0x04
if isinstance(beat.hasHarmonic, gp.ArtificialHarmonic):
flags1 |= 0x08
if beat.effect.fadeIn:
flags1 |= 0x10
if beat.effect.isTremoloBar or beat.effect.isSlapEffect:
flags1 |= 0x20
if beat.effect.stroke != gp.BeatStroke():
flags1 |= 0x40
self.writeByte(flags1)
if flags1 & 0x20:
self.writeByte(beat.effect.slapEffect.value)
self.writeTremoloBar(beat.effect.tremoloBar)
if flags1 & 0x40:
self.writeBeatStroke(beat.effect.stroke)
def writeTremoloBar(self, tremoloBar):
if tremoloBar is not None:
self.writeInt(tremoloBar.value)
else:
self.writeInt(0)
def writeBeatStroke(self, stroke):
if stroke.direction == gp.BeatStrokeDirection.up:
strokeUp = self.fromStrokeValue(stroke.value)
strokeDown = 0
elif stroke.direction == gp.BeatStrokeDirection.down:
strokeUp = 0
strokeDown = self.fromStrokeValue(stroke.value)
self.writeSignedByte(strokeDown)
self.writeSignedByte(strokeUp)
def fromStrokeValue(self, value):
if value == gp.Duration.hundredTwentyEighth:
return 1
elif value == gp.Duration.sixtyFourth:
return 2
elif value == gp.Duration.thirtySecond:
return 3
elif value == gp.Duration.sixteenth:
return 4
elif value == gp.Duration.eighth:
return 5
elif value == gp.Duration.quarter:
return 6
else:
return 1
def writeMixTableChange(self, tableChange):
self.writeMixTableChangeValues(tableChange)
self.writeMixTableChangeDurations(tableChange)
def writeMixTableChangeValues(self, tableChange):
self.writeSignedByte(tableChange.instrument.value if tableChange.instrument is not None else -1)
self.writeSignedByte(tableChange.volume.value if tableChange.volume is not None else -1)
self.writeSignedByte(tableChange.balance.value if tableChange.balance is not None else -1)
self.writeSignedByte(tableChange.chorus.value if tableChange.chorus is not None else -1)
self.writeSignedByte(tableChange.reverb.value if tableChange.reverb is not None else -1)
self.writeSignedByte(tableChange.phaser.value if tableChange.phaser is not None else -1)
self.writeSignedByte(tableChange.tremolo.value if tableChange.tremolo is not None else -1)
self.writeInt(tableChange.tempo.value if tableChange.tempo is not None else -1)
def writeMixTableChangeDurations(self, tableChange):
if tableChange.volume is not None:
self.writeSignedByte(tableChange.volume.duration)
if tableChange.balance is not None:
self.writeSignedByte(tableChange.balance.duration)
if tableChange.chorus is not None:
self.writeSignedByte(tableChange.chorus.duration)
if tableChange.reverb is not None:
self.writeSignedByte(tableChange.reverb.duration)
if tableChange.phaser is not None:
self.writeSignedByte(tableChange.phaser.duration)
if tableChange.tremolo is not None:
self.writeSignedByte(tableChange.tremolo.duration)
if tableChange.tempo is not None:
self.writeSignedByte(tableChange.tempo.duration)
def writeNotes(self, beat):
stringFlags = 0x00
for note in beat.notes:
stringFlags |= 1 << (7 - note.string)
self.writeByte(stringFlags)
for note in sorted(beat.notes, key=lambda note: note.string):
self.writeNote(note)
def writeNote(self, note):
flags = self.packNoteFlags(note)
self.writeByte(flags)
if flags & 0x20:
self.writeByte(note.type.value)
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 & 0x08:
self.writeNoteEffects(note)
def packNoteFlags(self, note):
flags = 0x00
try:
if note.duration is not None and note.tuplet is not None:
flags |= 0x01
except AttributeError:
pass
if note.effect.heavyAccentuatedNote:
flags |= 0x02
if note.effect.ghostNote:
flags |= 0x04
if not note.effect.isDefault:
flags |= 0x08
if note.velocity != gp.Velocities.default:
flags |= 0x10
flags |= 0x20
return flags
def writeNoteEffects(self, note):
noteEffect = note.effect
flags1 = 0x00
if noteEffect.isBend:
flags1 |= 0x01
if noteEffect.hammer:
flags1 |= 0x02
if gp.SlideType.shiftSlideTo in noteEffect.slides or gp.SlideType.legatoSlideTo in noteEffect.slides:
flags1 |= 0x04
if noteEffect.letRing:
flags1 |= 0x08
if noteEffect.isGrace:
flags1 |= 0x10
self.writeByte(flags1)
if flags1 & 0x01:
self.writeBend(noteEffect.bend)
if flags1 & 0x10:
self.writeGrace(noteEffect.grace)
def writeBend(self, bend):
self.writeSignedByte(bend.type.value)
self.writeInt(bend.value)
self.writeInt(len(bend.points))
for point in bend.points:
self.writeInt(round(point.position * self.bendPosition / gp.BendEffect.maxPosition))
self.writeInt(round(point.value * self.bendSemitone / gp.BendEffect.semitoneLength))
self.writeBool(point.vibrato)
def writeGrace(self, grace):
self.writeSignedByte(grace.fret)
self.writeByte(self.packVelocity(grace.velocity))
self.writeByte(8 - bit_length(grace.duration))
self.writeSignedByte(grace.transition.value)
def packVelocity(self, velocity):
velocityIncrement = gp.Velocities.velocityIncrement
minVelocity = gp.Velocities.minVelocity
return int((velocity + velocityIncrement - minVelocity) / velocityIncrement)