Updated documentation and improved offitracker.py code
This commit is contained in:
parent
80961a5d3f
commit
c5e72e662d
@ -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
73
docs/creating-songs.md
Normal 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
91
docs/library-usage.md
Normal 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.")
|
||||||
|
```
|
@ -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.")
|
||||||
|
Loading…
Reference in New Issue
Block a user