/ NeoTrellis_M4_MIDI_Synth / synth.py
synth.py
1 # SPDX-FileCopyrightText: 2018 Dave Astels for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 """ 6 NeoTrellis M4 Express MIDI synth 7 8 Adafruit invests time and resources providing this open source code. 9 Please support Adafruit and open source hardware by purchasing 10 products from Adafruit! 11 12 Written by Dave Astels for Adafruit Industries 13 Copyright (c) 2018 Adafruit Industries 14 Licensed under the MIT license. 15 16 All text above must be included in any redistribution. 17 """ 18 19 # pylint: disable=unused-argument 20 21 import time 22 import board 23 import audioio 24 import audiocore 25 import audiomixer 26 27 SAMPLE_FOLDER = '/samples/' # the name of the folder containing the samples 28 VOICE_COUNT = 8 29 30 def capitalize(s): 31 if not s: 32 return '' 33 return s[0].upper() + ''.join([x.lower() for x in s[1:]]) 34 35 36 class Synth(object): 37 38 def __init__(self): 39 self._voice_name = None 40 self._voice_file = None 41 self._samples = [None] * 128 42 self._channel_count = None 43 self._bits_per_sample = None 44 self._sample_rate = None 45 self._audio = None 46 self._mixer = None 47 self._currently_playing = [{'key': None, 'voice' : x} for x in range(VOICE_COUNT)] 48 self._voices_used = 0 49 50 def _initialize_audio(self): 51 if self._audio is None: 52 self._audio = audioio.AudioOut(board.A1) 53 self._mixer = audiomixer.Mixer(voice_count=VOICE_COUNT, 54 sample_rate=16000, 55 channel_count=1, 56 bits_per_sample=16, 57 samples_signed=True) 58 self._audio.play(self._mixer) 59 60 def reset(self): 61 for i in range(len(self._samples)): 62 self._samples[i] = None 63 for p in self._currently_playing: 64 p['key'] = None 65 66 @property 67 def voice(self): 68 return self._voice_name 69 70 @voice.setter 71 def voice(self, v): 72 self._initialize_audio() 73 self._voice_name = capitalize(v) 74 self._voice_file = '/samples/%s.txt' % v.lower() 75 first_note = None 76 with open(self._voice_file, "r") as f: 77 for line in f: 78 cleaned = line.strip() 79 if len(cleaned) > 0 and cleaned[0] != '#': 80 key, filename = cleaned.split(',', 1) 81 self._samples[int(key)] = filename.strip() 82 if first_note is None: 83 first_note = filename.strip() 84 sound_file = open(SAMPLE_FOLDER+first_note, 'rb') 85 wav = audiocore.WaveFile(sound_file) 86 self._mixer.play(wav, voice=0, loop=False) 87 time.sleep(0.5) 88 self._mixer.stop_voice(0) 89 90 def _find_usable_voice_for(self, key): 91 if self._voices_used == VOICE_COUNT: 92 return None 93 available = None 94 for voice in self._currently_playing: 95 if voice['key'] == key: 96 return None 97 if voice['key'] is None: 98 available = voice 99 if available is not None: 100 self._voices_used += 1 101 return available 102 return None 103 104 def _find_voice_for(self, key): 105 for voice in self._currently_playing: 106 if voice['key'] == key: 107 return voice 108 return None 109 110 def note_on(self, key, velocity): 111 fname = self._samples[key] 112 if fname is not None: 113 f = open(SAMPLE_FOLDER+fname, 'rb') 114 wav = audiocore.WaveFile(f) 115 voice = self._find_usable_voice_for(key) 116 if voice is not None: 117 voice['key'] = key 118 voice['file'] = f 119 self._mixer.play(wav, voice=voice['voice'], loop=False) 120 121 def note_off(self, key, velocity): 122 if self._voices_used > 0: 123 voice = self._find_voice_for(key) 124 if voice is not None: 125 self._voices_used -= 1 126 self._mixer.stop_voice(voice['voice']) 127 voice['file'].close() 128 voice['file'] = None 129 voice['key'] = None