Tabalchi Documentation

(C) Shreyan Mitra

Installation

To install the Tabalchi package, use the command:

$ pip install Tabalchi

Classes


BeatRange


A class representing an interval of beats.



Parameters
  • begin (int): The start beat of the beat range, inclusive.
  • end (int): The end beat of the beat range, exclusive.


Functions (Selected if too many to write)
  • __init__(self, begin:int, end:int):

    Initializes a beat range with the given start (inclusive) and end (exclusive).

  • fromString(self, spec:str) -> BeatRange:

    Returns a BeatRange object from a string specification.

  • range(self) -> int:

    Returns the number of beats represented by this BeatRange.

  • isContiguousSequence(cls, ranges:List[BeatRange], totalBeats:int) -> bool:

    Determines if the provided beat ranges cover a contiguous sequence from 1 to the total number of beats.

  • getSubsequence(cls, ranges:List[BeatRange], begin:int, end:int) -> List[BeatRange]:

    Returns a sorted list of beat ranges between a given beginning and end beat.


Full Code


# A class representing an interval of beats
class BeatRange():
    '''
    Class representing a beat range

    Parameters:
        begin(int): The start beat of the beat range, inclusive
        end(int): The end beat of the beat range, exclusive
    '''
    def __init__(self, begin:int, end:int):
        assert begin < end, "BeatRange end beat must be greater than begin beat"
        self.begin = begin
        self.end = end

    @classmethod
    def fromString(self, spec:str) -> BeatRange:
        numbers = spec.split("-")
        num1 = int(numbers[0])
        num2 = int(numbers[1])
        return BeatRange(num1, num2)

    def range(self) -> int:
        '''
        Returns the number of beats represented by this beat range
        '''
        return self.end - self.begin

    @classmethod
    def isContiguousSequence(cls, ranges:List[BeatRange], totalBeats:int) -> bool:
        '''
        Returns if a particular beat range convers all beats from 1 to the given total number of beats

        Parameters:
            ranges(List[BeatRange]): A list of beat ranges
            totalBeats(int): The total number of beats in the sequence to check the ranges against
        '''
        ranges = sorted(ranges, lambda range: range.begin)
        for i in range(1, len(ranges)):
            if ranges[i].begin != ranges[i-1].end:
                return False
        if(ranges[-1].end < totalBeats or ranges[0].begin != 1):
            return False
        return True

    @classmethod
    def getSubsequence(cls, ranges:List[BeatRange], begin:int, end:int) -> List[BeatRange]:
        '''
        Returns the ranges, in sorted order, that fall between a given begin and end beat

        Parameters:
            ranges(List[BeatRange]): A list of beat ranges to choose from
            begin(int): The start beat of the desired sequence
            end(int): The end beat of the desired sequence
        '''
        subsequence = []
        for i in range(len(ranges)):
            range = ranges[i]
            if range.begin >= begin and range.end <= end:
                subsequence.append(range)
            elif range.begin >= begin and range.end > end:
                subsequence.append(BeatRange(range.begin, end))
            elif range.begin < begin and range.end <= end:
                subsequence.append(BeatRange(begin, range.end))
            else:
                subsequence.append(BeatRange(begin, end))
        return sorted(subsequence)
                



CompositionType

A class representing a composition type. Ex. Kayda, Rela, etc.


Class Variables (Selected if too many to write)
  • registeredTypes: A class variable keeping track of the list of registered composition types.

Parameters
  • name (str): The name of the composition type.
  • schema (dict): The structure of the components field of the .tabla file.
  • validityCheck (Callable[[Bol],[bool]]): A function that checks if a given Bol is of the considered composition type.
  • assembler (Callable[[SimpleNamespace], [list[str]]]): Instructions on how to assemble components of the composition.
  • register (bool): Whether to register the composition type.

Functions (Selected if too many to write)
  • __init__(self, name:str, schema:dict, validityCheck:Callable[[Bol],[bool]], assembler:Callable[[SimpleNamespace], [list[str]]], register:bool = True):

    Initializes a CompositionType object with the given parameters.


Full Code

# A class representing a composition type. Ex. Kayda, Rela, etc.
# For descriptions of the different types of tabla compositions, visit www.tablalegacy.com (not affiliated with this product or the author in any way)
# Sometimes, differences between types of compositions are hard to quantify, and come down to the "feel" of the composition.
class CompositionType():
    registeredTypes = {} # A class variable keeping track of the list of registered composition types

    '''
    A class to represent a composition type

    Parameters:
        name(str): The name of the composition type. Ex. Kayda, Rela, etc.
        schema(dict): The structure of the components field of the .tabla file
        validityCheck(Callable[[Bol],[bool]]): A function that returns whether a given Bol is of the composition type being considered
        assembler(Callable[[SimpleNamespace], [list[str]]]): Gives instructions on how to put together the disjointed components of the composition
        register(bool): Whether to register the composition type (i.e. to save it for future use). By default, True
    '''
    def __init__(self, name:str, schema:dict, validityCheck:Callable[[Bol],[bool]], assembler:Callable[[SimpleNamespace], [list[str]]], register:bool = True):
        self.name = name
        self.schema = schema
        self.assembler = assembler
        def preValidityCheck(bol:dict) -> bool:
            try:
                validate(instance = bol, schema = schema)
                return True
            except Exception:
                return False

        self.preCheck = preValidityCheck
        self.mainCheck = validityCheck
        if register:
            CompositionType.registeredTypes.update({name: self})
                


Numeric

A base class representing something that has an associated number. Implemented as an abstract base class (ABC).


Functions (Selected if too many to write)
  • name (abstract property):

    A property that subclasses must implement, representing the name of the numeric entity.

  • number (abstract property):

    A property that subclasses must implement, representing the number associated with the entity.


Full Code

# A class representing something with an associated number. Ex. Taal, Jati, Speed, etc.
class Numeric(ABC):
    '''
    A class representing something that has an associated number
    '''
    @property
    @abstractmethod
    def name(self):
        ...

    @property
    @abstractmethod
    def number(self):
        ...
                


Taal

A class representing a Taal. Ex. Teental, Rupaak, etc.


Class Variables (Selected if too many to write)
  • registeredTaals: A class variable keeping track of the list of registered taals.

Parameters
  • beats (int): The number of beats in the taal.
  • taali (list[int]): A list of beats where the clap is marked.
  • khali (list[int]): A list of beats where the wave (khali) is marked.
  • name (Union[str, None]): The name of the taal, defaults to number of beats if not provided.
  • theka (Union[str, None]): The standard theka for the taal.
  • register (bool): Whether to register the taal (default: True).

Functions (Selected if too many to write)
  • __init__(self, beats:int, taali:list[int] = [], khali:list[int] = [], name:Union[str, None] = None, theka:Union[str, None] = None, register:bool = True):

    Initializes a Taal object with the given parameters and registers it if specified.

  • name (property):

    Returns the name of the taal.

  • number (property):

    Returns the number of beats in the taal.

  • theka (property):

    Returns the theka of the taal.


Full Code

# A class representing a Taal
class Taal(Numeric):
    registeredTaals = {}
    '''
    A class representing a taal. Ex. Teental, Rupaak, etc.
    '''
    def __init__(self, beats:int, taali:list[int] = [], khali:list[int] = [], name:Union[str, None] = None, theka:Union[str, None] = None, register:bool = True):
        self.beats = beats
        self.taali = taali
        self.khali = khali
        if not name:
            self.id = str(beats)
        else:
            self.id = name
        self.theka = theka
        if register:
            Taal.registeredTaals.update({self.id: self})

    @property
    def name(self):
        return self.id

    @property
    def number(self):
        return self.beats

    @property
    def theka(self):
        return self.theka
                


Jati

A class representing a Jati, which denotes the number of syllables per beat.


Class Variables (Selected if too many to write)
  • registeredJatis: A class variable keeping track of the list of registered Jatis.

Parameters
  • syllables (int): The number of syllables in this Jati.
  • name (Union[str, None]): The name of the Jati, defaults to number of syllables if not provided.
  • register (bool): Whether to register the Jati (default: True).

Functions (Selected if too many to write)
  • __init__(self, syllables:int, name:Union[str, None] = None, register = True):

    Initializes a Jati object with the given parameters and registers it if specified.

  • name (property):

    Returns the name of the Jati.

  • number (property):

    Returns the number of syllables in the Jati.


Full Code

# A class representing a jati
class Jati(Numeric):
    registeredJatis = {}
    '''
    A class representing a Jati
    '''
    def __init__(self, syllables:int, name:Union[str, None] = None, register = True):
        self.syllables = syllables
        if not name:
            self.id = str(syllables)
        else:
            self.id = name
        if register:
            Jati.registeredJatis.update({self.id: self})

    @property
    def name(self):
        return self.id

    @property
    def number(self):
        return self.syllables
                


SpeedClasses

A class representing different categories of speed for a composition.


Class Variables (Selected if too many to write)
  • registeredSpeeds: A class variable keeping track of the list of registered speed classes.

Functions (Selected if too many to write)
  • __init__(self, inClassCheck: Callable[[int], bool], randomGenerate: Callable[[], int], name: str, register: bool = True):

    Initializes a speed class with checks and generators for speed.

  • getSpeedClassFromBPM(cls, bpm: int) -> str:

    Returns the name of the speed class based on beats per minute (BPM).


Full Code
# A class that represents a speed category
class SpeedClasses:
   registeredSpeeds = {}
   '''
   A class representing a Speed class
   '''
   def __init__(self, inClassCheck: Callable[[int], bool], randomGenerate:Callable[[], int], name:str, register:bool = True):
       self.check = inClassCheck
       self.generator = randomGenerate
       self.id = name
       if register:
           SpeedClasses.registeredSpeeds.update({name: self})

   @classmethod
   def getSpeedClassFromBPM(cls, bpm:int) -> str:
       for key, value in SpeedClasses.registeredSpeeds.items():
           if value.check(bpm):
               return key
               


Speed

A class representing a specific speed in beats per minute (BPM).


Parameters
  • specifier (Union[int, str]): Either an integer for BPM or a string specifying the speed class.

Functions (Selected if too many to write)
  • __init__(self, specifier: Union[int, str]):

    Initializes a Speed object based on the specified BPM or speed class.

  • name (property):

    Returns the name of the speed class.

  • number (property):

    Returns the BPM.


Full Code
# A class that represents a specific speed
class Speed(Numeric):
   '''
   Class that represents a particular speed. Ex. 62bpm
   '''
   def __init__(self, specifier:Union[int, str]):
       if isinstance(specifier, str):
           self.name = specifier
           self.bpm = SpeedClasses[specifier].generator()
       else:
           self.bpm = specifier
           self.name = SpeedClasses.getSpeedClassFromBPM(specifier)

   @property
   def name(self):
       return self.name

   @property
   def number(self):
       return self.bpm
               


Notation

An abstract base class for all types of notations.


Functions (Selected if too many to write)
  • toString(cls, bol: Bol):

    An abstract method to be implemented by subclasses to convert a Bol to string.

  • display(cls, bol: Bol, fileName: str):

    Displays the notation to a specified file.


Full Code
class Notation(ABC):
   VALID_NOTATIONS = ["Bhatkande", "Paluskar"]

   @classmethod
   @abstractmethod
   def toString(self, bol:Bol):
       ...

   @classmethod
   def display(cls, bol:Bol, fileName:str):
       print(Notation.toString(bol), file=fileName)
               


Bol

A class representing a Bol, which is a collection of beats in a tabla composition.


Parameters
  • beats (list[Beat]): A list of Beat objects that constitute the bol.
  • notationClass (Union[Type[Notation], None]): The notation class used for displaying the bol.

Functions (Selected if too many to write)
  • __init__(self, beats: list[Beat], notationClass: Union[Type[Notation], None] = None):

    Initializes a Bol object with the specified beats and optional notation.

  • play(self):

    Plays the entire Bol composition.

  • write(self, filename: str, notationClass: Union[Type[Notation], None]):

    Writes the Bol to a file using the desired notation.


Full Code
class Bol():
   '''
   A class representing a bol, a collection of beats
   '''
   def __init__(self, beats:list[Beat], notationClass:Union[Type[Notation], None] = None):
       self.beats = beats
       self.notationClass = notationClass
       self.markedBeats = []
       self.markedPhrases = []
       for beat in beats:
           for i in range(len(beat.phrases.keys())):
               lst = list(beat.phrases.keys())
               if(beat.markers[i] == 1):
                   self.markedBeats.append(beat)
                   self.markedPhrases.append(lst[i])

   def play(self):
       for beat in self.beats:
           beat.play()

   def write(self, filename:str, notationClass:Union[Type[Notation], None]):
       if notationClass is not None:
           notationClass.display(self, filename)
       elif self.notationClass is not None:
           self.notationClass.display(self, filename)
       else:
           raise ValueError("No Notation object found to use.")
               


Beat

A class representing a collection of phrases in a beat.


Parameters
  • number (int): The number of the beat.
  • taaliKhaliOrNone (Literal[-1,0,1]): Indicator for clapped (1), waved (0), or neutral (-1).
  • saam (bool): Indicates if this is a starting beat.
  • phrases (OrderedDict[Phrase, int]): Phrases present in this beat with associated syllables.
  • speed (int): The speed of the beat in BPM.
  • markers (list[Literal[0,1]]): Markers indicating noteworthy phrases within the beat.

Functions (Selected if too many to write)
  • __init__(...):

    Initializes a Beat object with the specified parameters.

  • play(self):

    Plays the sound files corresponding to the phrases in the beat.


Full Code
class Beat():
                 '''
    A class representing a collection of phrases
    '''
    def __init__(self, number:int, taaliKhaliOrNone:Literal[-1,0,1], saam:bool, phrases:OrderedDict[Phrase, int], speed:int, markers:list[Literal[0,1]]):
        self.number = number
        assert len(markers) == len(phrases.keys()), "Invalid length for marker array"
        self.markers = markers
        self.clap = taaliKhaliOrNone
        self.saam = saam
        self.speed = speed
        duration = 60.0/speed #In seconds (this is the duration of the entire beat)
        jati = 0
        for _,val in phrases.items():
            jati += val
        syllableDuration = duration/jati #This is duration of a specific segment of the beat
        self.multipliers = []
        self.soundFiles = []
        for phrase, syllables in phrases.items():
            self.multipliers.append((syllables*1.0)/phrase.syllables) * (syllableDuration/0.25)) #Since, in the original recording, one syllable = 0.25 seconds
            self.soundFiles.append(phrase.soundBite.recording)
        self.phrases = phrases

    def play(self):
        for index in range(len(self.soundFiles)):
            s = AudioSegment(self.soundFiles[i])
            if self.multipliers[i] >= 1:
                s = s.speedup(self.multipliers[i])
            else:
                s = ae.speed_down(s, self.multipliers[i])
            pydubplay(s)
                    


Fetcher

A class containing static methods to fetch sounds and Variables (Selected if too many to write).


Functions (Selected if too many to write)
  • fetch(cls, id, specifier=None, componentIDs=None) -> Sound:

    Fetches a sound object given its identifier, or synthesizes it from components if necessary.

  • addRecording(cls, file):

    Adds an audio file recording to the recordings folder.


Full Code
class Fetcher:
    #Class that contains several static methods involving fetching sounds and Variables (Selected if too many to write)
    @classmethod
    def fetch(cls, id, specifier = None, componentIDs = None) -> Sound:
        '''
        Fetch the Sound object given a phrase identifier, or synthesize it from componentIDs given a specifier

        Parameters:
            id(string): The identifier for the sound
            specifier(string or None): If the sound does not exist and needs to be specified, whether the phrase is a composite or sequential phrase
            componentIDs(list[string] or None): A list of the identifiers making up a composite or sequential phrases

        Returns:
            newSound (Sound) OR oldSound (Sound): The Sound instance representing the given id
        '''
        oldSound = Sound.sounds.get(id)
        if oldSound:
            return oldSound
        elif not specifier:
            raise ValueError("Did not find soundbite. Have you preregistered the id? Otherwise, you should just pass the soundBite when initializing the phrase.")
        elif specifier == "composite":
            assert componentIDs, "Need to specify component ids for composite phrases."
            newSound = Sound(id, Sound.merge(Sound.sounds.get(c) for c in componentIDs))
            return newSound
        elif specifier == "sequential":
            newSound = Sound(id, Sound.join(Sound.sounds.get(c) for c in componentIDs))
            return newSound

    @classmethod
    def addRecording(cls, file):
        '''
        A method to add an audio file recording to the recordings folder.
            file(str): Path to MIDI file
        '''
        os.rename(file, "recordings/" + os.path.basename(file))
                    


Sound

A class representing a soundbite associated with a phrase on the tabla.


Class Variables (Selected if too many to write)
  • sounds (dict): Stores all instantiated sounds.

Parameters
  • id (string): The unique identifier of the soundbite.
  • recording (string): The file name of an audio file in the recordings folder.

Functions (Selected if too many to write)
  • __init__(self, id, recording):

    Initializes a Sound object with the specified ID and recording file.

  • play(self):

    Plays the sound represented by this Sound object.

  • merge(cls, sounds) -> str:

    For composite sounds, plays all sounds simultaneously and returns the new file name.

  • join(cls, sounds) -> str:

    For sequential sounds, plays all sounds one after the other and returns the new file name.


Full Code
class Sound():
    sounds = {}
    '''
    Class that represents the soundbite associated with a particular phrase

    Class Variables (Selected if too many to write):
        sounds(dict): stores all instantiated sounds

    Parameters:
        id(string): The unique identifier of the soundbite, typically the name of the associated phrase
        recording(string): The file name of a audio file in the recordings/ folder. The reocrding must be 0.25 second per syllable, i.e. equivalent to playing Chatusra Jati at 60 bpm
    '''
    def __init__(self, id, recording):
        self.id = id
        self.recording = "recordings/" + recording
        Sound.sounds.update({id: self})

    def play(self):
        '''
        Plays the sound represented by this Sound object
        '''
        playsound(self.recording)

    @classmethod
    def merge(cls, sounds) -> str:
        '''
        For composite sounds, play all the sounds simultaneously
        '''
        assert len(sounds) > 1, "More than 1 sound must be provided to merge"
        mergedSound = AudioSegment.from_file(sounds[0].recording)
        fileName = sounds[0].id
        for i in range(1, len(sounds)):
            mergedSound = mergedSound.overlay(AudioSegment.from_file(sounds[i].recording), position = 0)
            fileName += "+" + sounds[i].id

            fileName = "recordings/" + fileName + ".m4a"
            mergedSound.export(fileName, format = "ipod")
            return fileName

        @classmethod
        def join(cls, sounds) -> str:
            '''
            For sequential sounds, play all the sounds one after the other, in the order given

            Parameters:
            sounds(list[Sound]): the individual component sounds to play

            Returns:
            newRecording(string): An audio file containing the combination requested
            '''
            assert len(sounds) > 1, "More than 1 sound must be provided to join"
            mergedSound = AudioSegment.from_file(sounds[0].recording)
            fileName = sounds[0].id
            for i in range(1, len(sounds)):
                mergedSound = mergedSound + AudioSegment.from_file(sounds[i].recording)
                fileName += sounds[i].id
            fileName = "recordings/" + fileName + ".m4a"
            mergedSound.export(fileName, format = "ipod")
            return fileName
                        


Phrase

A class representing a phrase on the tabla.


Class Variables (Selected if too many to write)
  • registeredPhrases: The phrases that have been registered so far.

Parameters
  • mainID (string): The name of the phrase.
  • syllables (int): Number of syllables in this phrase.
  • position (string): Position where this phrase is played (baiyan, daiyan, or both).
  • info (string): Information about how to play this phrase.
  • aliases (Optional[List[str]]): Other names for this phrase in compositions.
  • soundBite (Union[str, Sound]): Either "Fetch" if sound is preregistered or a path/Sound object.
  • register (bool): Whether this phrase should be registered.

Functions (Selected if too many to write)
  • __init__(self, mainID, syllables=1, position='baiyan', info='No info provided', aliases=None, soundBite='Fetch', register=True):

    Initializes a Phrase object with the specified parameters and registers it if specified.

  • play(self):

    Plays the sound associated with this Phrase object.

  • createCompositePhrase(cls, mainID, componentIDs, aliases=None, soundBite='Fetch', register=True):

    Creates a composite Phrase given component phrases.

  • createSequentialPhrase(cls, mainID, componentIDs, position, aliases=None, soundBite='Fetch', register=True):

    Creates a sequential Phrase given component phrases.


Full Code
class Phrase():
        registeredPhrases = {}  # The phrases that have been registered so far

        '''
        Class that represents a phrase on the tabla
        '''
        def __init__(self, mainID, syllables=1, position='baiyan', info='No info provided', aliases=None, soundBite='Fetch', register=True):
            if not isinstance(soundBite, Sound) and soundBite != "Fetch":
                soundBite = Sound(mainID, soundBite)
            mainID = mainID.lower()
            self.ids = [mainID]
            if aliases:
                self.ids += aliases
            self.description = f"Phrase: {self.ids}\nPlayed on {position}.\n{info}\nNo. of syllables: {syllables}"
            self.syllables = syllables
            self.position = position
            self.info = info
            self.soundBite = soundBite if soundBite != "Fetch" else Fetcher.fetch(mainID)
            if register:
                for id in self.ids:
                    Phrase.registeredPhrases.update({id: self})

        def __repr__(self):
            return str(self.ids[0])

        def play(self):
            self.soundBite.play()

        @classmethod
        def createCompositePhrase(cls, mainID, componentIDs, aliases=None, soundBite='Fetch', register=True):
            '''
            Creates a composite phrase

            Parameters:
            componentIDs(list): IDs of the component phrases that make up this composite phrase
            '''
            assert len(componentIDs) == 2, "A composite phrase must have exactly 2 component phrases"
            component1, component2 = map(Phrase.registeredPhrases.get, componentIDs)
            assert component1.position != component2.position and component1.position in ["baiyan", "daiyan"] and component2.position in ["baiyan", "daiyan"], "Components must be played on different drums"
            x = cls(mainID=mainID,
                    syllables=max(component1.syllables, component2.syllables),
                    position="both drums",
                    info=f"Play the following two phrases simultaneously: \n1) {component1.info}\n2) {component2.info}",
                    aliases=aliases,
                    soundBite=soundBite if soundBite else Fetcher.fetch(mainID, "composite", componentIDs),
                    register=register
            )
            return x

        @classmethod
        def createSequentialPhrase(cls, mainID, componentIDs, position, aliases=None, soundBite='Fetch', register=True):
            '''
            Creates a sequential phrase

            Parameters:
            componentIDs(list): IDs of the sequential phrases that make up this composite phrase
            '''
            assert all(id in Phrase.registeredPhrases for id in componentIDs), "Must register component phrases first."
            syllables = sum(Phrase.registeredPhrases[id].syllables for id in componentIDs)
            info = "Play the following phrases in succession:" + "".join(
                f"\n{i}) {Phrase.registeredPhrases[componentIDs[i]].info}" for i in range(len(componentIDs)))
            x = cls(mainID=mainID,
                    syllables=syllables,
                    position=position,
                    info=info,
                    aliases=aliases,
                    soundBite=soundBite if soundBite else Fetcher.fetch(mainID, "sequential", componentIDs),
                    register=register
            )
            return x
                        


CompositionGenerator

A class that provides static methods for generating compositions using the Llama model.


Functions (Selected if too many to write)
  • generate(cls, type:str, taal:Union[str, int], speedClass:str, jati:Union[str, int], school:str, token:str):

    Generates a composition given parameters using the Llama model available on HuggingFace.


Full Code
class CompositionGenerator():
        # Class that provides a static method to generate a composition
        @classmethod
        def generate(cls, type:str, taal:Union[str, int], speedClass: str, jati: Union[str, int], school: str, token: str):
            '''
            A method that generates a composition given parameters using the Llama model available on HuggingFace
            '''
            warnings.warn("This is an experimental feature that may provide incorrect or incomplete results.")
            warnings.warn("Execution time might be excessive depending on your hardware.")
            phraseInfo = "The following phrases are defined by the user on the tabla, along with a description of how to play them: \n" + "\n".join([key + "." + val.description for key, val in Phrase.registeredPhrases.items()])
            mainPrompt = "Using the above phrases only, compose a " + type + " in taal with name/beats " + taal + " and speed class " + speedClass + ". The composition should be in jati with name/syllables per beat " + jati + " and in the " + school + " style of playing. Components of the composition should be marked appropriately."
            symbolPrompt = "Each beat should be separated with the character '|'. An example of the expected output if the user requests a Kayda of Ektaal, with Chatusra Jati, in the Lucknow Gharana is: \n" + open("template.tabla, "r").read() + "\n A phrase cannot span more than one beat. A phrase can also span exactly one syllable even if it usually spans more than one. In that case, enclose the phrase with parentheses."
        end = "Finally, in addition to following the above rules, the composition should be as authentic and aesthetically pleasing as possible."
        prompt = phraseInfo + mainPrompt + symbolPrompt + end
        messages = [
            {"role": "user", "content": prompt},
        ]
        pipe = pipeline("text-generation", model="meta-llama/Meta-Llama-3-70B-Instruct", token = token, torch_dtype=torch.float16, device_map="auto")
        return pipe(messages, do_sample = True, num_return_sequences = 1, eos_token_id = pipe.tokenizer.eos_token_id, return_full_text = False)[0]['generated_text']
                    


AudioToBolConvertor

A class providing static methods to transcribe a bol from an audio recording.


Functions (Selected if too many to write)
  • convert(cls, recording:str, speed:int, jati:int) -> str:

    Generates the bol transcription from an audio recording.

  • getMostSimilarSound(cls, snippet, from:Dict[str, str]) -> str:

    Gets the most similar sounding phrase to a given audio snippet.


Full Code
class AudioToBolConvertor():
    # Class that provides a static method to transcribe a bol given the recording of a composition
    @classmethod
    def convert(cls, recording:str, speed:int, jati:int) -> str:
        '''
        A method that generates the bol given an audio recording
        '''
        warnings.warn("This is an experimental feature that may provide incorrect or incomplete results.")
        currentSyllableDuration = 60.0/(speed*jati)
        desiredSyllableDuration = 0.25
        sound = AudioSegment.from_file(recording)
        if currentSyllableDuration > desiredSyllableDuration:
            sound = sound.speedup(currentSyllableDuration / desiredSyllableDuration)
        elif currentSyllableDuration < desiredSyllableDuration:
            sound = ae.speed_down(sound, currentSyllableDuration / desiredSyllableDuration)
        # Parse the audio for every 0.25 second snippet, comparing it to known recordings
        recordings = {val.soundBite.recording: key for key, val in Phrase.registeredPhrases.items()}
        bolString = ""
        marker = 0
        while marker < sound.duration_seconds * 1000:
            add = cls.getMostSimilarSound(snippet=sound[marker: marker + 250], from=recordings)
            marker += Phrase.registeredPhrases[add].syllables * 250
            bolString += add
        return bolString

    @classmethod
    def getMostSimilarSound(cls, snippet, from:Dict[str, str]) -> str:
        '''
        Gets the most similar sounding bol to a given audio snippet
        '''
        snippet.export("snippetTemp", format = "m4a")
        _, encoded = acoustid.fingerprint_file("snippetTemp")
        fingerprint, _ = chromaprint.decode_fingerprint(encoded)
        references = {}
        for key, val in from.items():
            _, e = acoustid.fingerprint_file(key)
            f, _ = chromaprint.decode_fingerprint(e)
            references[val] = f

        from operator import xor
        maxSimilarity = 0
        mostSimilarPhrase = None
        for phrase, print in references.items():
            max_hamming_weight = 32 * min(len(fingerprint), len(print))
            hamming_weight = sum(
                sum(c == "1" for c in bin(xor(fingerprint[i], print[i])))
                for i in range(min(len(fingerprint), len(print)))
            )
            if (hamming_weight / max_hamming_weight) > maxSimilarity:
                maxSimilarity = hamming_weight / max_hamming_weight
                mostSimilarPhrase = phrase
        return mostSimilarPhrase
                    


BolParser

A class that parses a .tabla file and converts it to a concise, playable form.


Class Constants and Variables (Selected if too many to write)
  • BEAT_DIVIDER: The character used to differentiate beats.
  • PHRASE_SPLITTER: Indicates a sequential phrase split to match jati.

Functions (Selected if too many to write)
  • parse(cls, file) -> Bol:

    Parses a .tabla file and returns a Bol object representing the composition.


Full Code
class BolParser():
    '''
    Class that parses a .tabla file and converts it to a concise, playable form
    '''
    BEAT_DIVIDER = "|"
    PHRASE_SPLITTER = "-"
    PHRASE_JOINER_OPEN = "["
    PHRASE_JOINER_CLOSE = "]"
    MARKER = "~"

    # Download recordings folder if it does not exist already
    destination = Path.cwd() / "recordings"
    destination.mkdir(exist_ok=True, parents=True)
    fs = fsspec.filesystem("github", org="shreyanmitra", repo="Tabalchi")
    fs.get(fs.ls("recordings/"), destination.as_posix(), recursive=True)

    # Initialize components like phrases, composite phrases, sequential phrases, etc.
    # Example of initializing phrases (complete this based on code):

    # Method to parse .tabla file into an object of Bol
    @classmethod
    def parse(cls, file) -> Bol:
        '''
        Parses a .tabla file and returns a Bol object
        '''
        assert ".tabla" in file, "Please pass a valid .tabla file"
        with open(file, 'r') as composition:
            data = json.load(composition)
            data = SimpleNamespace(**data)
        try:
            compositionType = CompositionType.registeredTypes[data.type]
            taal = Taal.registeredTaals[data.taal]
            # Detailed parsing logic for speed, jati, and so forth
            # Uses pre-defined schemas, conditions, and associations to validate and parse
        except Exception:
            raise ValueError("Something is wrong with the configuration of your .tabla file")

                    

```