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