Skip to content
This repository has been archived by the owner on Dec 21, 2021. It is now read-only.

Commit

Permalink
new synthesizer API
Browse files Browse the repository at this point in the history
  • Loading branch information
irmen committed Jul 26, 2018
1 parent 583e0db commit cf9f2c3
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 29 deletions.
2 changes: 1 addition & 1 deletion bouldercaves/synthplayer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "1.1"
__version__ = "1.2"
__author__ = "Irmen de Jong"
6 changes: 3 additions & 3 deletions bouldercaves/synthplayer/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Written by Irmen de Jong ([email protected]) - License: GNU LGPL 3.
"""

norm_samplerate = 44100
norm_nchannels = 2
norm_samplewidth = 2
norm_samplerate = 44100 # CD quality
norm_nchannels = 2 # two channel stereo
norm_samplewidth = 2 # samples are 2 bytes = 16 bits
norm_frames_per_chunk = norm_samplerate // 30 # about 6kb worth of data, 1/30 sec
81 changes: 57 additions & 24 deletions bouldercaves/synthplayer/playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
import audioop # type: ignore
import queue
import threading
import tempfile
import time
import os
import io
from collections import defaultdict
from typing import Generator, Union, Dict, Tuple, Any, Type, List, Callable, Iterable, Optional
from .import params
Expand Down Expand Up @@ -374,12 +373,15 @@ def audio_thread():
try:
while True:
data = b""
repeat = False
command = None
try:
command = self.command_queue.get(timeout=0.2)
if command is None or command["action"] == "stop":
break
elif command["action"] == "play":
data = command["sample"].view_frame_data() or b""
repeat = command["repeat"]
except queue.Empty:
self.all_played.set()
data = b""
Expand All @@ -388,6 +390,21 @@ def audio_thread():
if self.playing_callback:
sample = Sample.from_raw_frames(data, self.samplewidth, self.samplerate, self.nchannels)
self.playing_callback(sample)
if repeat:
# remove all other samples from the queue and reschedule this one
commands_to_keep = []
while True:
try:
c2 = self.command_queue.get(block=False)
if c2["action"] == "play":
continue
commands_to_keep.append(c2)
except queue.Empty:
break
for cmd in commands_to_keep:
self.command_queue.put(cmd)
if command:
self.command_queue.put(command)
finally:
self.all_played.set()
stream.stop()
Expand All @@ -399,7 +416,7 @@ def audio_thread():

def play(self, sample: Sample, repeat: bool=False, delay: float=0.0) -> int:
self.all_played.clear()
self.command_queue.put({"action": "play", "sample": sample})
self.command_queue.put({"action": "play", "sample": sample, "repeat": repeat})
return 0

def silence(self) -> None:
Expand Down Expand Up @@ -516,12 +533,15 @@ def audio_thread():
try:
while True:
data = b""
repeat = False
command = None
try:
command = self.command_queue.get(timeout=0.2)
if command is None or command["action"] == "stop":
break
elif command["action"] == "play":
data = command["sample"].view_frame_data() or b""
repeat = command["repeat"]
except queue.Empty:
self.all_played.set()
data = b""
Expand All @@ -532,6 +552,21 @@ def audio_thread():
if self.playing_callback:
sample = Sample.from_raw_frames(data, self.samplewidth, self.samplerate, self.nchannels)
self.playing_callback(sample)
if repeat:
# remove all other samples from the queue and reschedule this one
commands_to_keep = []
while True:
try:
c2 = self.command_queue.get(block=False)
if c2["action"] == "play":
continue
commands_to_keep.append(c2)
except queue.Empty:
break
for cmd in commands_to_keep:
self.command_queue.put(cmd)
if command:
self.command_queue.put(command)
finally:
self.all_played.set()
stream.close()
Expand All @@ -544,7 +579,7 @@ def audio_thread():

def play(self, sample: Sample, repeat: bool=False, delay: float=0.0) -> int:
self.all_played.clear()
self.command_queue.put({"action": "play", "sample": sample})
self.command_queue.put({"action": "play", "sample": sample, "repeat": repeat})
return 0

def silence(self) -> None:
Expand Down Expand Up @@ -596,8 +631,9 @@ def __init__(self, samplerate: int=0, samplewidth: int=0, nchannels: int=0, queu
import winsound as _winsound
global winsound
winsound = _winsound # type: ignore
self.threads = [] # type: List[threading.Thread]
self.played_callback = None
self.sample_queue = queue.Queue(maxsize=queue_size) # type: queue.Queue[Sample]
threading.Thread(target=self._play, daemon=True).start()

def play(self, sample: Sample, repeat: bool=False, delay: float=0.0) -> int:
# plays the sample in a background thread so that we can continue while the sound plays.
Expand All @@ -606,35 +642,32 @@ def play(self, sample: Sample, repeat: bool=False, delay: float=0.0) -> int:
raise ValueError("winsound player doesn't support repeating samples")
if delay != 0.0:
raise ValueError("winsound player doesn't support delayed playing")
self.wait_all_played()
t = threading.Thread(target=self._play, args=(sample,), daemon=True)
self.threads.append(t)
t.start()
time.sleep(0.0005)
self.sample_queue.put(sample)
return 0

def _play(self, sample: Sample) -> None:
with tempfile.NamedTemporaryFile(delete=False, mode="wb") as sample_file:
sample.write_wav(sample_file) # type: ignore
sample_file.flush()
winsound.PlaySound(sample_file.name, winsound.SND_FILENAME) # type: ignore
if self.played_callback:
self.played_callback(sample)
os.unlink(sample_file.name)
def _play(self) -> None:
# this runs in a background thread so a sample playback doesn't block the program
# (can't use winsound async because we're playing from a memory buffer)
while True:
sample = self.sample_queue.get()
with io.BytesIO() as sample_data:
sample.write_wav(sample_data) # type: ignore
winsound.PlaySound(sample_data.getbuffer(), winsound.SND_MEMORY) # type: ignore
if self.played_callback:
self.played_callback(sample)

def stop(self, sid_or_name: Union[int, str]) -> None:
raise NotImplementedError("sequential play mode doesn't support stopping individual samples")
raise NotImplementedError("winsound sequential play mode doesn't support stopping individual samples")

def set_sample_play_limit(self, samplename: str, max_simultaneously: int) -> None:
raise NotImplementedError("sequential play mode doesn't support setting sample limits")
raise NotImplementedError("winsound sequential play mode doesn't support setting sample limits")

def wait_all_played(self) -> None:
while self.threads:
t = self.threads.pop()
t.join()
while not self.sample_queue.empty():
time.sleep(0.2)

def still_playing(self) -> bool:
return bool(self.threads)
return not self.sample_queue.empty()


class Output:
Expand Down
8 changes: 7 additions & 1 deletion bouldercaves/synthplayer/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ class StreamingSample(Sample):
def __init__(self, wave_file: Union[str, BinaryIO]=None, name: str="") -> None:
super().__init__(wave_file, name)

def view_frame_data(self):
raise NotImplementedError("a streaming sample doesn't have a frame data buffer to view")

def load_wav(self, file_or_stream: Union[str, BinaryIO]) -> 'Sample':
self.wave_stream = wave.open(file_or_stream, "rb")
if not 2 <= self.wave_stream.getsampwidth() <= 4:
Expand All @@ -245,7 +248,10 @@ def chunked_frame_data(self, chunksize: int, repeat: bool=False,
while True:
audiodata = self.wave_stream.readframes(chunksize // self.samplewidth // self.nchannels)
if not audiodata:
break # source stream exhausted
if repeat:
self.wave_stream.rewind()
else:
break # non-repeating source stream exhausted
if len(audiodata) < chunksize:
audiodata += silence[len(audiodata):]
yield memoryview(audiodata)
Expand Down

0 comments on commit cf9f2c3

Please sign in to comment.