diff --git a/README.md b/README.md index 5c337f1..3574e91 100644 --- a/README.md +++ b/README.md @@ -3,33 +3,54 @@ **py_guitar_synth** is a Python package for generating realistic guitar music from tab sheets by modeling the physical and musical characteristics of guitar strings and performance techniques. The project allows users to convert guitar tabs into audio, simulating strokes, note durations, transitions, and string properties. +
+ +
+ ## Features - Convert guitar tab sheets into realistic audio signals. -- Model various string characteristics such as inharmonicity, decay rates, and vibrato. -- Apply impulse response convolution to simulate room acoustics. -- Add custom echo effects to audio output. -- Support for multiple instruments (guitar, violin, piano). +- Model various string characteristics such as _inharmonicity_, _decay rates_, and _vibrato_. +- Apply _impulse response convolution_ to simulate room acoustics. +- Add custom _echo effects_ to audio output. +- Support for multiple instruments (classical guitar, violin, piano). - Built-in JSON-based configuration for instruments. - Easily extendable for new instruments and custom sounds. [Check out the documentation here](/docs/index.rst) +## Play Samples + +**Osad Eini with a classical guitar** + + + +**Osad Eini with a violin** + + + ## Installation You can clone this repository and install the package locally: ```bash -git clone https://github.com/your-repo-url/py_guitar_synth.git +git clone https://github.com/mustafaalotbah/py_guitar_synth.git cd py_guitar_synth pip install . ``` ## Dependencies -- Python 3.6+ +- Python 3.9+ - `numpy` - `soundfile` +- `sounddevice` Ensure all dependencies are installed by running: @@ -100,25 +121,112 @@ signal = generate_guitar_signal_from_sheet( ) ``` +### Music Sheet Tab Format + +`py_guitar_synth` supports a structured format for music sheets, which includes both metadata and the tab itself. +The metadata section provides information about the piece, such as the title, author, tempo (in beats per minute), and any capo settings. +The tab section follows, specifying the notes for each string in traditional guitar tab notation. + +**Example** + +```text +Title: Agua Marina +Author: Paco Cepero +bpm: 90 +capo fret: 2 + +# This is a comment + +e |----3-2-3-2!3!2!-0++---| |----3-2-3-2!3!2!-0++-| |-0-2-0-2-0!2!0!------| +B |-0---------------------| |-0-------------------| |-----------------4++-| +G |-----------------------| |---------------------| |---------------------| +D |-----------------2+r---| |-----------------2+r-| |-----------------1+r-| +A |-----------------------| |---------------------| |---------------------| +E |-----------------------| |---------------------| |---------------------| +``` + +#### Metadata Section: + +- **Title**: The title of the composition. In this example, it is _Agua Marina_. +- **Author**: The composer or arranger, here _Paco Cepero_, known for his contributions to flamenco music. +- **bpm (beats per minute)**: The tempo of the piece, which dictates the pacing of the performance. A tempo of `90 bpm` indicates a moderately slow pace, typical for expressive styles like ballads or slower flamenco pieces. +- **Capo Fret**: The fret position where the capo is placed. The capo at the second fret transposes all notes up by two semitones, effectively raising the pitch of the entire performance without altering the fingering patterns. + +#### Tablature Section: + +The tablature (or tab) notation directly corresponds to the guitar fretboard and specifies both pitch (fret number) and duration (note value) for each note. + +- **Strings (e, B, G, D, A, E)**: These represent the six strings of the guitar, with `e` being the high e string (1st string) and `E` the low E string (6th string). + +- **Fret Numbers**: The numbers indicate which fret to press on a given string. For example, `3` means pressing the 3rd fret. + +- **Rhythmic Symbols**: + + - **`!`**: Marks shorter rhythmic values, such as eighth notes (`!`) or sixteenth notes (`!!`), providing finer rhythmic granularity. + - **`++`**: Indicates longer note values like whole notes (`++`) or half notes (`+`), corresponding to sustained pitches that resonate longer. + - **`r`**: The "let ring" indicator, meaning the note should continue to resonate after being plucked, producing a sustained tone. +- **Bars (`|----|`)**: The vertical bars divide the tab into readable measures but serve only as a visual guide. The width of each section between bars has no impact on timing or structure in the `py_guitar_synth` system, meaning the spacing is purely for organizational clarity. The actual note durations are encoded using symbols such as `!` or `++`. + ## Command-Line Interface (CLI) -You can use the package from the command line with the guitar_synth command to generate a guitar signal. +The `py_guitar_synth` package allows users to generate realistic guitar or other instrument sounds from a musical sheet, either predefined or from a file. +The command-line interface (CLI) provides various options to customize the output, including instrument choice, sound effects, and more. + +#### Usage + +```shell +python -m py_guitar_synth [-h] [-i {classical_guitar,violin,piano}] [-s SHEET] [-p PLUCK_POSITION] [--sr SR] [--no-convolution] [--ir-file IR_FILE] [--no-echo] [--echo-delay ECHO_DELAY] [--echo-decay ECHO_DECAY] +``` + ```shell -guitar_synth --instrument path/to/instrument.json --sheet path/to/tab.txt --output output.wav +python -m py_guitar_synth --instrument path/to/instrument.json --sheet path/to/tab.txt --output output.wav ``` #### CLI Options: -- `--instrument` : Path to the JSON file defining the instrument's properties (e.g., guitar, violin). -- `--sheet` : Path to the guitar tab file or the name of a predefined sheet. -- `--output` : Path where the generated WAV file will be saved. -- `--convolve` : Whether to apply an impulse response (room simulation). Default is `True`. -- `--impulse-response` : Path to a custom impulse response WAV file for convolution. -- `--echo` : Whether to add an echo effect. Default is `True`. -- `--echo-delay` : Set the delay time for the echo (default: 0.2 seconds). -- `--echo-decay` : Set the decay factor for the echo (default: 0.2). -- `--pluck-position` : Set the pluck position on the string (default: 0.7). +- **`-i {classical_guitar, violin, piano}` / `--instrument {classical_guitar, violin, piano}`**: + This flag allows you to specify the instrument for the synthesis. The tool supports three default instruments: + + - `classical_guitar`: The default instrument, which simulates the sound of a nylon-string classical guitar. + - `violin`: A simulation of the violin, capturing the unique characteristics of string-based sound generation. + - `piano`: Simulates piano keys, focusing on producing hammer-strike sounds instead of string plucks. + +- **`-s SHEET` / `--sheet SHEET`**: + This option allows you to specify either a predefined sheet or the path to a custom tab file. + + - Predefined options include `law_bass_f_aini`, `agua_marina` and `osad_eini`, which are stored in the tool as examples. + - If you prefer to use your custom music, provide the file path to a valid guitar tab sheet. The tool will parse the file and synthesize the corresponding sound. + +- **`-p PLUCK_POSITION` / `--pluck_position PLUCK_POSITION`**: + The `pluck_position` parameter represents the point along the length of the string where it is plucked, and it significantly influences the harmonic content of the sound produced. + For instance, plucking near the bridge emphasizes higher harmonics, while plucking closer to the midpoint (e.g., 0.5) suppresses some harmonics. + + - The value is a floating point number between `0` and `1`, where `0` represents plucking near the bridge (brighter sound), and `1` represents plucking near the neck (mellower sound). + - The default value is `0.7`, which corresponds to plucking closer to the middle of the string, providing a balanced sound. +- **`--sr SR` / `--sampling-rate SR`**: + This option controls the sample rate (`SR`) of the generated audio, in Hertz. The sample rate determines the audio quality and the size of the generated file. + + - The default value is `44100 Hz`, which is standard CD quality, but users can increase this for higher fidelity or reduce it for faster processing and smaller file sizes. +- **`--no-convolution`**: + By default, the tool applies an impulse response convolution to the audio, which simulates the acoustic properties of an environment (e.g., a guitar body, concert hall). This flag disables that effect, resulting in a raw, "dry" sound that has no added reverberation or acoustic characteristics. + +- **`--ir-file IR_FILE`**: + This option allows you to provide a custom impulse response (IR) file in WAV format. An impulse response is a way to simulate how sound behaves in a specific acoustic space, like a room or inside an instrument's body. + + - If you have a custom impulse response, you can specify its path here to customize the acoustic environment applied to the sound synthesis. If this flag is not used, the default impulse response (if convolution is enabled) will be applied. +- **`--no-echo`**: + This flag disables the echo effect applied to the sound. The echo effect simulates a delay or reverberation, adding depth and realism to the generated sound. + + - Disabling this will result in a more direct, clean sound without any trailing reflections. +- **`--echo-delay ECHO_DELAY`**: + If echo is enabled (by default), this option allows you to set the delay time between the original sound and the first echo. The value represents time in seconds (e.g., `0.2` seconds by default). + + - A higher delay creates a more pronounced echo, simulating a larger room or a greater distance between reflective surfaces. +- **`--echo-decay ECHO_DECAY`**: + The `echo_decay` parameter controls how quickly the echo fades out after being heard. A value closer to `0` results in a faster fade, while higher values produce a longer-lasting echo. The default decay factor is `0.2`. + + - This gives control over how much reverb or echo is applied, allowing for subtle or more prominent effects, depending on the user’s preference. Example usage: diff --git a/docs/index.rst b/docs/index.rst index d2d0fd3..0263cbd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,13 @@ -py_guitar_synth +Py Guiter Synthesizer =============== Welcome to the `py_guitar_synth` project documentation! This project focuses on synthesizing realistic guitar sounds based on guitar tab sheets, simulating the physical and musical characteristics of strings and performance techniques. +.. image:: /docs/logo.png + :alt: py_guitar_synth Logo + :align: center + :scale: 10% + .. contents:: Table of Contents :depth: 3 :local: @@ -105,11 +110,26 @@ Notes played simultaneously across different strings can be written in a vertica This represents an E minor chord. -Example: +A chord can also be played while the hand is gradually plucking the strings, resulting in a technique known as arpeggiation or a rolled chord. +This technique involves plucking each string in quick succession rather than striking all the strings at once. +In the following example, the "r" symbol indicates that each note should "let ring," meaning the string should continue to resonate after being plucked. .. code-block:: bash - e|-3!-3r--| # Eighth note followed by let ring + e |----------------------3++r-| + B |------------------3!!r-----| + G |--------------0!!r---------| + D |----------0!!r-------------| + A |-----2!!r------------------| + E |-3!!r----------------------| + +This notation represents a G major chord, but instead of strumming all the strings simultaneously, the player gradually plucks each string, +starting from the low E string to the high e string. As each note is played, the previous ones continue to resonate due to the "r" (letRing=True), +allowing for a smooth, cascading effect as the chord is built up across the strings. + +The numbers represent the frets, while the symbols (e.g., ++, !!) indicate the duration of each note, from longer whole notes (++) to shorter sixteenth notes (!!). +This rolling technique creates a more textured and dynamic sound, often used in classical and fingerstyle guitar playing. + Custom Instrument Creation --------------------------- diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..974b05c Binary files /dev/null and b/docs/logo.png differ diff --git a/media/osad_eini_classical_guitar.mp3 b/media/osad_eini_classical_guitar.mp3 new file mode 100644 index 0000000..cbe459c Binary files /dev/null and b/media/osad_eini_classical_guitar.mp3 differ diff --git a/media/osad_eini_violin.mp3 b/media/osad_eini_violin.mp3 new file mode 100644 index 0000000..1f5e939 Binary files /dev/null and b/media/osad_eini_violin.mp3 differ diff --git a/py_guitar_synth/__main__.py b/py_guitar_synth/__main__.py index 9d15c8c..b7ea8d4 100644 --- a/py_guitar_synth/__main__.py +++ b/py_guitar_synth/__main__.py @@ -1,5 +1,7 @@ import argparse import sounddevice as sd +import threading + from py_guitar_synth import ( default_classical_guitar, default_violine, @@ -23,6 +25,7 @@ 'agua_marina': agua_marina } + def main(): parser = argparse.ArgumentParser( description="py_guitar_synth: A tool for synthesizing guitar audio from tab sheets." @@ -86,6 +89,8 @@ def main(): # Load the selected instrument and sheet instrument = INSTRUMENTS[args.instrument] + print(f"Reading Sheet {args.sheet}") + # Load the sheet, either from predefined sheets or from a file if args.sheet in SHEETS: sheet = SHEETS[args.sheet] @@ -110,10 +115,23 @@ def main(): echo_decay=args.echo_decay ) - # Play the generated audio using sounddevice - print(f"Playing {args.sheet} with {args.instrument}...") - sd.play(signal, args.sr) - sd.wait() + def play(): + # Play the generated audio using sounddevice + print(f"Playing '{sheet.title}' by '{sheet.author}' with {args.instrument}...") + sd.play(signal, args.sr) + sd.wait() + + # Start the playback in a separate thread + play_thread = threading.Thread(target=play) + play_thread.start() + + # Main thread continues to run + try: + while play_thread.is_alive(): + play_thread.join(timeout=1) + except KeyboardInterrupt: + sd.stop() + print("Playback stopped.") if __name__ == '__main__': diff --git a/py_guitar_synth/assets/osad_eini.txt b/py_guitar_synth/assets/osad_eini.txt new file mode 100644 index 0000000..1a13681 --- /dev/null +++ b/py_guitar_synth/assets/osad_eini.txt @@ -0,0 +1,50 @@ +# This Music sheet is written by Mustafa Alotbah +# + +Title: Osad Eini +Author: Amr Diab +bpm: 55 +capo fret: 3 + +e |-----------------------| |-----------------------| |----------------------------| +B |--0!1!0r-----0!2r-2++--| |-1-1!0---0-0!1!3!1-1++-| |-1-1!0-1!-3+-3!2!!1!!0-0++--| +G |---------2-0-0!2r-2++--| |-------2---------------| |----------------------------| +D |-2-----------0+r-------| |-----------------------| |----------------------------| +A |-----------------------| |-----------------------| |----------------------------| +E |-----------------------| |-----------------------| |----------------------------| + +e |-----------------------| |-----------------------| |----------------------------| +B |-0-0!1!0r-----0!2++r---| |-----------------------| |----------------------------| +G |----------2-0-0!2++r---| |-2-2!0--2!4+-4!5!4-4++-| |4!-4!2-2!0-0-0!2++-2!4!2-2++| +D |--------------0++r-----| |-----------------------| |----------------------------| +A |-----------------------| |-----------------------| |----------------------------| +E |-----------------------| |-----------------------| |----------------------------| + +e |-----------------------| |-----------------0++r--| |----------------------------| +B |-0-0!1!0r-----0!2++r---| |-------------0!!r------| |-0!!1!!3!!5++-5!-5!3-3!1-1++| +G |----------2-0-0!2++r---| |---------2!!r----------| |----------------------------| +D |--------------0++r-----| |-----2!!r--------------| |----------------------------| +A |-----------------------| |-0!!r------------------| |----------------------------| +E |-----------------------| |-----------------------| |----------------------------| + +e |-----------------------| |-----------------------| |----------------------------| +B |-3!1!0-1-1!3++---------| |-3!-3!1-1!0-0+---------| |---0-0!1!3!1++--------------| +G |-----------------------| |-----------------------| |-2--------------------------| +D |-----------------------| |-----------------------| |----------------------------| +A |-----------------------| |-----------------------| |----------------------------| +E |-----------------------| |-----------------------| |----------------------------| + +e |-----------------------| |-----------------------| |----------------------------| +B |--10--0-1-3-3!5++------| |-0-1-3-3!5++--5++------| |-5!-5!6!5!-3!5!3!r----------| +G |-----2-----------------| |-----------------------| |-------------------5++------| +D |-----------------------| |-----------------------| |----------------------------| +A |-----------------------| |-----------------------| |----------------------------| +E |-----------------------| |-----------------------| |----------------------------| + +e |-----------------------| |-----------------------| |----------------------------| +B |-3!1!0-1-1!3++---------| |-1!-3!5!3!-1!3!1!------| |----------------------------| +G |-----------------------| |------------------4++--| |----------------------------| +D |-----------------------| |-----------------------| |----------------------------| +A |-----------------------| |-----------------------| |----------------------------| +E |-----------------------| |-----------------------| |----------------------------| + diff --git a/py_guitar_synth/sheets.py b/py_guitar_synth/sheets.py index cabbda4..197f095 100644 --- a/py_guitar_synth/sheets.py +++ b/py_guitar_synth/sheets.py @@ -33,4 +33,8 @@ # Load the 'agua_marina.txt' tab file with resources.as_file(resources.files('py_guitar_synth.assets').joinpath('agua_marina.txt')) as f: - agua_marina = parse_guitar_tab_from_file(str(f)) \ No newline at end of file + agua_marina = parse_guitar_tab_from_file(str(f)) + +# Load the 'agua_marina.txt' tab file +with resources.as_file(resources.files('py_guitar_synth.assets').joinpath('osad_eini.txt')) as f: + osad_eini = parse_guitar_tab_from_file(str(f)) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index dbfbdf9..93ee67e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ dependencies = [ "numpy", "soundfile", + "sounddevice", "argparse" ] diff --git a/requirements.txt b/requirements.txt index 3fd5690..980a920 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ numpy soundfile +sounddevice argparse importlib_resources; python_version < "3.10" diff --git a/setup.py b/setup.py index 0339761..82a3228 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ 'numpy', 'argparse', 'soundfile', + 'sounddevice', ], include_package_data=True, entry_points={