Updated documentation and improved offitracker.py code

This commit is contained in:
iAmInActions 2024-01-05 00:42:36 +01:00
parent 80961a5d3f
commit c5e72e662d
4 changed files with 196 additions and 19 deletions

View File

@ -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). 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.

73
docs/creating-songs.md Normal file
View File

@ -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.

91
docs/library-usage.md Normal file
View File

@ -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.")
```

View File

@ -5,20 +5,9 @@ import sounddevice as sd
import os import os
# OffiTracker, the tracker that no one asked for but I made it anyways :3 # 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: # 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.
# 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
# (c) 2024 mueller_minki, Feel free to modify or share. # (c) 2024 mueller_minki, Feel free to modify or share.
stop_signal = False stop_signal = False
noise_data_cache = {} # Cache to store loaded noise data 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) 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 stop_signal
global noise_data_cache 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 into the cache
load_all_noise_data() load_all_noise_data()
@ -81,18 +72,35 @@ def play_csv_file(file_path):
header = csv_reader.fieldnames header = csv_reader.fieldnames
num_columns = len(header) num_columns = len(header)
num_pairs = (num_columns - 1) // 2 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: 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)] 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)] effects = [float(row[f'Effect{i}']) for i in range(1, num_pairs + 1)]
duration = float(row['Duration']) duration = float(row['Duration'])
# Check if 'Noise' column exists in the CSV file # Check if 'Noise' column exists in the CSV file
noise_amplitude = float(row.get('Noise', 0)) 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: if stop_signal == False:
play_square_waves(output_stream, frequencies, effects, duration, noise_amplitude=noise_amplitude) play_square_waves(output_stream, frequencies, effects, duration, noise_amplitude=noise_amplitude)
if __name__ == "__main__": if __name__ == "__main__":
print(' ') print(' ')
print(' Mueller\'s Software Domain proudly presents:') print(' Mueller\'s Software Domain proudly presents:')
@ -102,10 +110,15 @@ if __name__ == "__main__":
print('/ | \ | | | | | | | | | \// __ \\\\ \___| <\ ___/| | \/') print('/ | \ | | | | | | | | | \// __ \\\\ \___| <\ ___/| | \/')
print('\_______ /__| |__| |__| |____| |__| (____ /\___ >__|_ \\\\___ >__| ') print('\_______ /__| |__| |__| |____| |__| (____ /\___ >__|_ \\\\___ >__| ')
print(' \/ \/ \/ \/ \/ ') print(' \/ \/ \/ \/ \/ ')
print(' Version 1.3') print(' Version 1.4')
if len(sys.argv) > 1: if len(sys.argv) > 1:
csv_file_path = sys.argv[1] csv_file_path = sys.argv[1]
else: else:
csv_file_path = input("Choose a CSV file: ") 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.")