Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed bug that caused all tracks to play to channel 1. Thus all track… #93

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ dist/
*.egg-info/

/venv

/.idea
!/mingus_examples/saved_blues.json
38 changes: 38 additions & 0 deletions doc/ccm_notes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Note Class:

requires some indication of the sound to play
pitch and instrument
percussion instrument (e.g. no pitch)

should be playable by itself (e.g. not as part of a bar)

optional params:
duration
velocity
channel
bank?
other params like pitch bend?


Bar Class:
Notes should be able to start in a bar and end in a different bar

a bar can have a meter, or inherit it from a track


Inheritence Precident:

Some params should be inherited from outer containers, unless they are specified.

For example, a track, contains a bar contains, a notecontainer contains, a note. If the track has
a channel, it should pass it to each bar. If a bar has a channel it should use it. But if the bar's
channel is None, it should inherit it from the track, etc...


******************************************************************
Listing all the instruments in a sound font file: https://github.com/FluidSynth/fluidsynth/wiki/UserManual#soundfonts

1. Open terminal
2. type: fluidsynth
3. type: load "path to sound font"
4. type: inst 1
2 changes: 1 addition & 1 deletion doc/wiki/tutorialExtraLilypond.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Tutorial 1 - Generating Sheet Music with LilyPond
=================================================

The LilyPond module provides some methods to help you generate files in the LilyPond format. This allows you to create sheet music from some of the objects in mingus.containers.
The LilyPond module provides some methods to help you generate files in the LilyPond format. This allows you to create sheet music from some of the objects in mingus.containers. Note: you need to install `LilyPond <http://lilypond.org/index.html>`_ on your system first.


>>> import mingus.extra.lilypond as LilyPond
Expand Down
2 changes: 1 addition & 1 deletion mingus/containers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from mingus.containers.note import Note
from mingus.containers.note import Note, PercussionNote
from mingus.containers.note_container import NoteContainer
from mingus.containers.bar import Bar
from mingus.containers.track import Track
Expand Down
126 changes: 94 additions & 32 deletions mingus/containers/bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,54 @@

import six

from mingus.containers import PercussionNote
from mingus.containers.mt_exceptions import MeterFormatError
from mingus.containers.note_container import NoteContainer
from mingus.core import meter as _meter
from mingus.core import progressions, keys
from typing import Optional

from mingus.containers.get_note_length import get_note_length, get_beat_start, get_bar_length


class Bar(object):
"""A bar object.

A Bar is basically a container for NoteContainers.
A Bar is basically a container for NoteContainers. This is where NoteContainers
get their duration.

Each NoteContainer must start in the bar, but it can end outside the bar.

Bars can be stored together with Instruments in Tracks.
"""

key = "C"
meter = (4, 4)
current_beat = 0.0
length = 0.0
bar = []

def __init__(self, key="C", meter=(4, 4)):
def __init__(self, key="C", meter=(4, 4), bpm=120, bars=None):
# warning should check types
if isinstance(key, six.string_types):
key = keys.Key(key)
self.key = key
self.bpm = bpm
self.set_meter(meter)
self.empty()

if bars:
self.bar = bars
self.current_beat = 0.0
else:
self.empty()

def to_json(self):
d = {
'class_name': self.__class__.__name__,
'key': self.key,
'meter': self.meter,
'bpm': self.bpm,
'bars': self.bar
}
return d

def empty(self):
"""Empty the Bar, remove all the NoteContainers."""
self.bar = []
self.current_beat = 0.0
self.bar = [] # list of [current_beat, note duration number, list of notes]
self.current_beat = 0.0 # fraction of way through bar
return self.bar

def set_meter(self, meter):
Expand All @@ -72,9 +87,9 @@ def set_meter(self, meter):
self.length = 0.0
else:
raise MeterFormatError(
"The meter argument '%s' is not an "
f"The meter argument {meter} is not an "
"understood representation of a meter. "
"Expecting a tuple." % meter
"Expecting a tuple."
)

def place_notes(self, notes, duration):
Expand All @@ -85,8 +100,7 @@ def place_notes(self, notes, duration):

Raise a MeterFormatError if the duration is not valid.

Return True if succesful, False otherwise (ie. the Bar hasn't got
enough room for a note of that duration).
Return True if successful, False otherwise if the note does not start in the bar.
"""
# note should be able to be one of strings, lists, Notes or
# NoteContainers
Expand All @@ -98,18 +112,13 @@ def place_notes(self, notes, duration):
notes = NoteContainer(notes)
elif isinstance(notes, list):
notes = NoteContainer(notes)
if self.current_beat + 1.0 / duration <= self.length or self.length == 0.0:

if self.is_full():
return False
else:
self.bar.append([self.current_beat, duration, notes])
self.current_beat += 1.0 / duration
return True
else:
return False

def place_notes_at(self, notes, at):
"""Place notes at the given index."""
for x in self.bar:
if x[0] == at:
x[2] += notes

def place_rest(self, duration):
"""Place a rest of given duration on the current_beat.
Expand All @@ -118,7 +127,8 @@ def place_rest(self, duration):
"""
return self.place_notes(None, duration)

def _is_note(self, note: Optional[NoteContainer]) -> bool:
@staticmethod
def _is_note(note: Optional[NoteContainer]) -> bool:
"""
Return whether the 'note' contained in a bar position is an actual NoteContainer.
If False, it is a rest (currently represented by None).
Expand Down Expand Up @@ -157,14 +167,14 @@ def change_note_duration(self, at, to):

def get_range(self):
"""Return the highest and the lowest note in a tuple."""
(min, max) = (100000, -1)
(min_note, max_note) = (100000, -1)
for cont in self.bar:
for note in cont[2]:
if int(note) < int(min):
min = note
elif int(note) > int(max):
max = note
return (min, max)
if int(note) < int(min_note):
min_note = note
elif int(note) > int(max_note):
max_note = note
return min_note, max_note

def space_left(self):
"""Return the space left on the Bar."""
Expand Down Expand Up @@ -223,6 +233,58 @@ def get_note_names(self):
res.append(x)
return res

def play(self, start_time: int, bpm: float, channel: int, score: dict) -> int:
"""
Put bar events into score.

:param start_time: start time of bar in milliseconds
:param bpm: beats per minute
:param channel: channel number
:param score: dict of events
:return: duration of bar in milliseconds
"""
assert type(start_time) == int

for bar_fraction, duration_type, notes in self.bar:
duration_ms = get_note_length(duration_type, self.meter[1], bpm)

current_beat = bar_fraction * self.meter[1] + 1.0
beat_start = get_beat_start(current_beat, bpm)
start_key = start_time + beat_start
end_key = start_key + duration_ms

if notes:
for note in notes:
score.setdefault(start_key, []).append(
{
'func': 'start_note',
'note': note,
'channel': channel,
'velocity': note.velocity
}
)

note_duration = getattr(note, 'duration', None)
if note_duration:
score.setdefault(start_key + note_duration, []).append(
{
'func': 'end_note',
'note': note,
'channel': channel,
}
)
elif not isinstance(note, PercussionNote):
score.setdefault(end_key, []).append(
{
'func': 'end_note',
'note': note,
'channel': channel,
}
)
else:
pass
return get_bar_length(self.meter, bpm)

def __add__(self, note_container):
"""Enable the '+' operator on Bars."""
if self.meter[1] != 0:
Expand Down
73 changes: 73 additions & 0 deletions mingus/containers/get_note_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from unittest import TestCase


def get_note_length(note_type, beat_length, bpm) -> int:
"""
Since we are working in milliseconds as integers, we want to unify how we calculate
note lengths so that tracks do not get out of sync

:param note_type: 1=whole note, 4 = quarter note, etc...
:param beat_length: 4 - quarter note, 8 - eighth note
:param bpm: beats per minute
:return: note length in milliseconds
"""
beat_ms = ((1.0 / bpm) * 60.0) * 1000.0 # milliseconds
length = (beat_length / note_type) * beat_ms
return round(length)


def get_beat_start(beat_number, bpm):
"""

:param beat_number: 1, 2, 3, 4 for 4/4, etc..
:param bpm: beats per minute
:return: note length in milliseconds
"""
beat_ms = ((1.0 / bpm) * 60.0) * 1000.0 # milliseconds
start = (beat_number - 1.0) * beat_ms
return round(start)


def get_bar_length(meter, bpm):
return get_beat_start(meter[0] + 1, bpm)


class TestLengthCalculations(TestCase):

def setUp(self) -> None:
super().setUp()
self.whole = 1.0
self.quarter = 4.0
self.eighth = 8.0

def test_get_note_length(self):
# A quarter note, in 4/4 with 1/2 second per beat
length = get_note_length(self.quarter, 4.0, 120.0)
self.assertEqual(500, length)

# A whole note, in 4/4 with 1/2 second per beat
length = get_note_length(self.whole, 4.0, 120.0)
self.assertEqual(2000, length)

# An eighth note, in 4/4 with 1/2 second per beat
length = get_note_length(self.eighth, 4.0, 120.0)
self.assertEqual(250, length)

# An eighth note, in 6/8 with 1/2 second per beat
length = get_note_length(self.eighth, 8.0, 120.0)
self.assertEqual(500, length)

# An quarter note, in 6/8 with 1/2 second per beat
length = get_note_length(self.quarter, 8.0, 120.0)
self.assertEqual(1000, length)

def test_get_beat_start(self):
start = get_beat_start(2.0, 120.0)
self.assertEqual(500, start)

def test_get_bar_length(self):
length = get_bar_length((4.0, 4.0), 120.0)
self.assertEqual(2000, length)

length = get_bar_length((6.0, 8.0), 120.0)
self.assertEqual(3000, length)
Loading