From c5e72e662d143bcc6673d2208dacfc1fa612c17e Mon Sep 17 00:00:00 2001 From: iAmInActions Date: Fri, 5 Jan 2024 00:42:36 +0100 Subject: [PATCH] Updated documentation and improved offitracker.py code --- README.md | 2 +- docs/creating-songs.md | 73 +++++++++++++++++++++++++++++++++ docs/library-usage.md | 91 ++++++++++++++++++++++++++++++++++++++++++ offitracker.py | 49 ++++++++++++++--------- 4 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 docs/creating-songs.md create mode 100644 docs/library-usage.md diff --git a/README.md b/README.md index bd519cd..d126ee4 100644 --- a/README.md +++ b/README.md @@ -37,4 +37,4 @@ Examples can be found in the `example` folder. A utility for converting midi files to csv can be found in the utility folder (monophonic only). -For usage information, check the contents of the `offitracker.py` file +Documentation on how to use offitracker as a library and on creating songs compatible with offitracker can be found in the `docs` folder. diff --git a/docs/creating-songs.md b/docs/creating-songs.md new file mode 100644 index 0000000..08e084e --- /dev/null +++ b/docs/creating-songs.md @@ -0,0 +1,73 @@ +# Creating songs in the OffiTracker format + +This document contains a quick tutorial on how to use the different features of the OffiTracker format to create music. + +## Spreadsheets galore + +The first step for your song is to create a new spreadsheet a program of your choice. + +It should be in the CSV format with comma separated columns. + +## Channel your creativity + +Like any tracker, OffiTracker uses multiple channels for playing different tones at once. + +Currently we only support square waves but this is subject to change soon. We will keep compatibility with existing songs however by making square waves the default. + +Each channel has a `Frequency` and an `Effect` column. `Frequency` is the notes frequency in Hz and `Effect` the pulse width of your square wave. + +You start counting your channels at 1 and can add as many as you like, just note that having more than 8 channels may result in bad audio quality or artifacting. + +Example: + +| Frequency1 | Effect1 | Frequency2 | Effect2 | +| ---------- | ------- | ---------- | ------- | +| 440 | 50 | 318 | 35 | + +## A matter of time + +The example above can obviously not work by itself yet as there is no way of knowing for how long to play the notes. + +For this, you use the `Duration` column. + +This column is recommended to be the furthest right one for consistency. + +`Duration` stores the time your row is played for in milliseconds. + +Example: + +| Frequency1 | Effect1 | Frequency2 | Effect2 | Duration | +| ---------- | ------- | ---------- | ------- | -------- | +| 440 | 50 | 318 | 35 | 80 | +| 519 | 50 | 411 | 28 | 114 | + +## Noisy company + +Having square waves is all fun and games but a good song also has drums. + +Introducing: The `Noise` column. + +The `Noise` column unlike the previously shown columns is optional. You do not need to include it in your file if you don't want to use it. + +There are 5 different noises that you can play back at the start of your row: + +1. Bass drum + +2. Kick drum + +3. Click + +4. Snare + +5. Hihat + +A value of 0 or no value will mean that no noise is played. + +Example: + +| Frequency1 | Effect1 | Frequency2 | Effect2 | Noise | Duration | +| ---------- | ------- | ---------- | ------- | ----- | -------- | +| 440 | 50 | 318 | 35 | 0 | 80 | +| 519 | 50 | 411 | 28 | 3 | 114 | + +Noises have their own duration ranging from long to short depending on the noise. In case the duration set in the `Duration` column is shorter than the noise, the noise will be cut off. diff --git a/docs/library-usage.md b/docs/library-usage.md new file mode 100644 index 0000000..27d4a05 --- /dev/null +++ b/docs/library-usage.md @@ -0,0 +1,91 @@ +# OffiTracker as a python library + +The OffiTracker program is designed in a way that allows it to be integrated in other projects by importing it. + +A reference design for this use case can be found in the `offiplayergui.py` file. + +## Importing + +The OffiTracker library can be imported by putting the line + +```python +import offitracker +``` + +in the head of your python project. For this to work, you need to place the `offitracker.py` file in your projects directory as well as the `drums` folder. + +## The stop signal + +Before we start playing anything, it would be useful to know how to stop the playback again. + +For that, OffiTracker has a `stop_signal` variable which we can change from outside. + +Example: + +```python +import offitracker as oftr + +# Set stop signal to False to allow for playback +oftr.stop_signal = False + +# Set stop signal to True to stop playback +oftr.stop_signal = True +``` + +## Playing a csv file + +Playing a csv file in the OffiTracker format can be done using the `play_csv_file` function. + +Example: + +```python +import offitracker as oftr + +# Set stop signal to False to allow for playback +oftr.stop_signal = False +# Start playing back a file +oftr.play_csv_file("example.csv") +``` + +## Playback position, threading + +The `play_csv_file` function has an optional `playback_row_index` variable that stores the currently playing row. If the variable is not initialized, the function will print a status message to stdout. Initializing it will disable that message and instead store the value which is useful when writing more complex software around the library. + +Example: + +```python +import offitracker as oftr +import threading +import time + +def playback_thread(csv_file_path): + # Set stop signal to False to allow for playback + oftr.stop_signal = False + # Initialise playback row index + oftr.playback_row_index = 0 + # Start playing back the file + oftr.play_csv_file(csv_file_path) + +# Example CSV file path +csv_file_path = "example.csv" + +# Create a thread for playback +playback_thread = threading.Thread(target=playback_thread, args=(csv_file_path,)) + +try: + # Start the playback thread + playback_thread.start() + + while not oftr.stop_signal: + # Read the current row index + current_row_index = oftr.playback_row_index + print(f"Current Row Index: {current_row_index}") + time.sleep(1) # Adjust the sleep duration as needed + +except KeyboardInterrupt: + # Set stop signal to True to stop playback when Ctrl+C is pressed + oftr.stop_signal = True + playback_thread.join() # Wait for the playback thread to finish + +print("Playback stopped.") +``` diff --git a/offitracker.py b/offitracker.py index 415f070..b32f3ea 100644 --- a/offitracker.py +++ b/offitracker.py @@ -5,20 +5,9 @@ import sounddevice as sd import os # OffiTracker, the tracker that no one asked for but I made it anyways :3 -# Usage: Make a CSV table in Excel or LibreOffice with the following format: -# Frequency1 Effect1 Frequency2 Effect2 .... Noise Duration -# You can make as many channels as you want. -# Effect = pulse width from 0 to 100 -# Frequency = tone in Hz. -# Noise: -# - 0 = No extra sound -# - 1 = Bass drum -# - 2 = Kick drum -# - 3 = Click -# - 4 = Snare -# - 5 = Hihat -# Duration = tone duration in ms +# This has started off as a silly little joke program, I never thought it would turn into such a complex little beast of a python project. # (c) 2024 mueller_minki, Feel free to modify or share. + stop_signal = False noise_data_cache = {} # Cache to store loaded noise data @@ -69,9 +58,11 @@ def play_square_waves(output_stream, frequencies, effects, duration, amplitude=1 output_stream.write(combined_wave) -def play_csv_file(file_path): +def play_csv_file(file_path, start_row=None, stop_row=None): global stop_signal global noise_data_cache + if 'playback_row_index' in locals(): + global playback_row_index # Load all noise data into the cache load_all_noise_data() @@ -81,18 +72,35 @@ def play_csv_file(file_path): header = csv_reader.fieldnames num_columns = len(header) num_pairs = (num_columns - 1) // 2 + total_rows = sum(1 for _ in csv_reader) # Count the total number of rows + + # Reset the file pointer to the beginning + csv_file.seek(0) + next(csv_reader) # Skip the header with sd.OutputStream(channels=1) as output_stream: - for row in csv_reader: + for idx, row in enumerate(csv_reader): + if start_row is not None and idx < start_row: + continue + if stop_row is not None and idx > stop_row: + break + frequencies = [float(row[f'Frequency{i}']) for i in range(1, num_pairs + 1)] effects = [float(row[f'Effect{i}']) for i in range(1, num_pairs + 1)] duration = float(row['Duration']) # Check if 'Noise' column exists in the CSV file noise_amplitude = float(row.get('Noise', 0)) + + # Update row info + if 'playback_row_index' in globals(): + playback_row_index = idx + else: + print(f"\rRow {idx + 1} of {total_rows}", end='', flush=True) + if stop_signal == False: play_square_waves(output_stream, frequencies, effects, duration, noise_amplitude=noise_amplitude) - + if __name__ == "__main__": print(' ') print(' Mueller\'s Software Domain proudly presents:') @@ -102,10 +110,15 @@ if __name__ == "__main__": print('/ | \ | | | | | | | | | \// __ \\\\ \___| <\ ___/| | \/') print('\_______ /__| |__| |__| |____| |__| (____ /\___ >__|_ \\\\___ >__| ') print(' \/ \/ \/ \/ \/ ') - print(' Version 1.3') + print(' Version 1.4') if len(sys.argv) > 1: csv_file_path = sys.argv[1] else: csv_file_path = input("Choose a CSV file: ") - play_csv_file(csv_file_path) + # These should not be set in player mode + start_row = None + stop_row = None + + play_csv_file(csv_file_path, start_row=start_row, stop_row=stop_row) + print("\nPlayback complete.")