Skip to content

Commit

Permalink
[#314] Closer to cassette player
Browse files Browse the repository at this point in the history
  • Loading branch information
vbmacher committed Apr 9, 2023
1 parent 80839cb commit d6f5d13
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.Queue;
import java.util.concurrent.*;

@ThreadSafe
public class CassetteController implements AutoCloseable {
Expand All @@ -41,10 +39,10 @@ public enum CassetteState {
UNLOADED,
PLAYING,
STOPPED,
CLOSED
CLOSED // terminal state
}

private final Loader.CassetteListener listener;
private final Loader.PlaybackListener listener;
private final ExecutorService playPool = Executors.newFixedThreadPool(1);

private final Object stateLock = new Object();
Expand All @@ -55,12 +53,14 @@ public enum CassetteState {
@GuardedBy("stateLock")
private Future<?> playFuture;

public CassetteController(Loader.CassetteListener listener) {
private final Queue<CassetteState> stateNotifications = new ConcurrentLinkedQueue<>();

public CassetteController(Loader.PlaybackListener listener) {
this.listener = Objects.requireNonNull(listener);
}

public CassetteState reset() {
return stop(true);
public void reset() {
stop(true);
}

@Override
Expand All @@ -73,7 +73,9 @@ public void close() {
tmpFuture.cancel(true);
}
playPool.shutdown();
stateNotifications.add(this.state);
}
notifyStateChange();
try {
if (!playPool.awaitTermination(5, TimeUnit.SECONDS)) {
playPool.shutdownNow();
Expand All @@ -83,27 +85,22 @@ public void close() {
}
}

public CassetteState load(Path path) {
Optional<CassetteState> optResult = Loader.create(path).map(tmpLoader -> {
public void load(Path path) {
Loader.create(path).ifPresent(tmpLoader -> {
synchronized (stateLock) {
switch (state) {
case UNLOADED:
case STOPPED:
this.loader = tmpLoader;
this.state = CassetteState.STOPPED;
}
return this.state;
stateNotifications.add(this.state);
}
});
if (optResult.isPresent()) {
return optResult.get();
}
synchronized (stateLock) {
return this.state;
}
notifyStateChange();
}

public CassetteState play() {
public void play() {
synchronized (stateLock) {
if (this.state == CassetteState.STOPPED) {
Loader tmpLoader = this.loader;
Expand All @@ -112,33 +109,49 @@ public CassetteState play() {
this.playFuture = playPool.submit(() -> {
try {
tmpLoader.load(listener);
synchronized (stateLock) {
this.state = CassetteState.STOPPED;
stateNotifications.add(this.state);
}
notifyStateChange();
} catch (IOException e) {
LOGGER.error("Could not load cassette", e);
Thread.currentThread().interrupt();
}
});
}
}
return this.state;
stateNotifications.add(this.state);
}
notifyStateChange();
}

public CassetteState stop(boolean unload) {
public void stop(boolean unload) {
synchronized (stateLock) {
if (this.state == CassetteState.PLAYING) {
Future<?> tmpFuture = this.playFuture;
this.playFuture = null;
if (tmpFuture != null) {
tmpFuture.cancel(true);
}
if (unload) {
this.loader = null;
this.state = CassetteState.UNLOADED;
} else {
this.state = CassetteState.STOPPED;
}
this.state = CassetteState.STOPPED;
}
if (unload && this.state == CassetteState.STOPPED) {
this.loader = null;
this.state = CassetteState.UNLOADED;
}
stateNotifications.add(this.state);
}
notifyStateChange();
}

public CassetteState getState() {
synchronized (stateLock) {
return this.state;
}
}

private void notifyStateChange() {
Optional.ofNullable(stateNotifications.poll()).ifPresent(listener::onStateChange);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class DeviceImpl extends AbstractDevice {

private CassettePlayerGui gui;
private CassetteController controller;
private CassetteListenerImpl cassetteListener;
private PlaybackListenerImpl cassetteListener;

public DeviceImpl(long pluginID, ApplicationApi applicationApi, PluginSettings settings) {
super(pluginID, applicationApi, settings);
Expand All @@ -56,7 +56,7 @@ public void initialize() throws PluginInitializationException {
if (lineIn.getDataType() != Byte.class) {
throw new PluginInitializationException("Could not find Byte device");
}
this.cassetteListener = new CassetteListenerImpl(lineIn);
this.cassetteListener = new PlaybackListenerImpl(lineIn);
this.controller = new CassetteController(cassetteListener);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,46 +24,56 @@

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

public class CassetteListenerImpl implements Loader.CassetteListener {
public class PlaybackListenerImpl implements Loader.PlaybackListener {
private final DeviceContext<Byte> lineIn;
private Optional<CassettePlayerGui> gui = Optional.empty();
private final AtomicReference<CassettePlayerGui> gui = new AtomicReference<>();

public CassetteListenerImpl(DeviceContext<Byte> lineIn) {
public PlaybackListenerImpl(DeviceContext<Byte> lineIn) {
this.lineIn = Objects.requireNonNull(lineIn);
}

public void setGui(CassettePlayerGui gui) {
this.gui = Optional.ofNullable(gui);
this.gui.set(gui);
}

@Override
public void onProgram(String filename, int dataLength, int autoStart, int programLength) {
gui.ifPresent(g -> g.setMetadata(filename));
log(filename + " : PROGRAM (start=" + autoStart + ", length=" + programLength + ")");
}

@Override
public void onNumberArray(String filename, int dataLength, char variable) {
gui.ifPresent(g -> g.setMetadata(filename));
log(filename + " : NUMBER ARRAY (variable=" + variable + ")");
}

@Override
public void onStringArray(String filename, int dataLength, char variable) {
gui.ifPresent(g -> g.setMetadata(filename));
log(filename + " : STRING ARRAY (variable=" + variable + ")");
}

@Override
public void onMemoryBlock(String filename, int dataLength, int startAddress) {
gui.ifPresent(g -> g.setMetadata(filename));
log(filename + " : MEMORY BLOCK (start=" + startAddress + ")");
}

@Override
public void onData(byte[] data) {

log("DATA");
}

@Override
public void onPause(int millis) {
gui.ifPresent(g -> g.setMetadata("PAUSE " + millis + "ms"));
Optional.ofNullable(gui.get()).ifPresent(g -> g.setMetadata("PAUSE " + millis + "ms"));
}

@Override
public void onStateChange(CassetteController.CassetteState state) {
Optional.ofNullable(gui.get()).ifPresent(g -> g.setCassetteState(state));
}

private void log(String message) {
Optional.ofNullable(gui.get()).ifPresent(g -> g.setMetadata(message));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public CassettePlayerGui(JFrame parent, Dialogs dialogs, CassetteController cont

initComponents();
setLocationRelativeTo(parent);
setCassetteState(controller.getState());
}

public void setMetadata(String metadata) {
Expand All @@ -65,6 +66,39 @@ public void setMetadata(String metadata) {
}
}

public void setCassetteState(CassetteController.CassetteState state) {
this.lblStatus.setText(state.name());
switch (state) {
case CLOSED:
btnLoad.setEnabled(false);
btnStop.setEnabled(false);
btnPlay.setEnabled(false);
btnUnload.setEnabled(false);
break;

case PLAYING:
btnPlay.setEnabled(false);
btnLoad.setEnabled(false);
btnUnload.setEnabled(true);
btnStop.setEnabled(true);
break;

case STOPPED:
btnStop.setEnabled(false);
btnLoad.setEnabled(true);
btnUnload.setEnabled(true);
btnPlay.setEnabled(true);
break;

case UNLOADED:
btnStop.setEnabled(false);
btnPlay.setEnabled(false);
btnLoad.setEnabled(true);
btnUnload.setEnabled(false);
break;
}
}

private void initComponents() {
JPanel panelTapeSelection = new JPanel();
JLabel lblTapes = new JLabel("Available tapes:");
Expand Down Expand Up @@ -93,18 +127,9 @@ private void initComponents() {
controller.load(lstTapesModel.getFilePath(index));
}
});
btnPlay.addActionListener(e -> {
CassetteController.CassetteState state = controller.play();
lblStatus.setText(state.name());
});
btnStop.addActionListener(e -> {
CassetteController.CassetteState state = controller.stop(false);
lblStatus.setText(state.name());
});
btnUnload.addActionListener(e -> {
CassetteController.CassetteState state = controller.stop(true);
lblStatus.setText(state.name());
});
btnPlay.addActionListener(e -> controller.play());
btnStop.addActionListener(e -> controller.stop(false));
btnUnload.addActionListener(e -> controller.stop(true));

panelTapeSelection.setBorder(BorderFactory.createTitledBorder("Tape selection"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package net.emustudio.plugins.device.cassette_player.gui;

import net.emustudio.plugins.device.cassette_player.loaders.Loader;
import net.jcip.annotations.NotThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -59,7 +60,10 @@ private static List<Path> listPaths(Path directory) {
return Collections.emptyList();
}
try(Stream<Path> stream = Files.list(directory)) {
return stream.collect(Collectors.toList());
return stream
.filter(Files::isReadable)
.filter(Loader::hasLoader)
.collect(Collectors.toList());
} catch (IOException e) {
LOGGER.error("Could not load tape files from directory: " + directory, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package net.emustudio.plugins.device.cassette_player.loaders;

import net.emustudio.plugins.device.cassette_player.CassetteController;
import net.jcip.annotations.ThreadSafe;

import java.io.IOException;
Expand All @@ -30,9 +31,22 @@
public interface Loader {

Map<String, Function<Path, Loader>> LOADERS = Map.of(
"tap", TapLoader::new
"tap", TapLoader::new,
"tzx", TzxLoader::new
);


static boolean hasLoader(Path path) {
int index = path.toString().lastIndexOf(".");
String extension = (index == -1) ?
"" : path.toString().substring(index + 1).toLowerCase(Locale.ENGLISH);

return LOADERS
.entrySet()
.stream()
.anyMatch(l -> l.getKey().equals(extension));
}

static Optional<Loader> create(Path path) {
int index = path.toString().lastIndexOf(".");
String extension = (index == -1) ?
Expand All @@ -48,7 +62,10 @@ static Optional<Loader> create(Path path) {
}

@ThreadSafe
interface CassetteListener {
interface PlaybackListener {

// tzx version

/**
* Data block will be a program in BASIC.
*
Expand Down Expand Up @@ -99,7 +116,12 @@ interface CassetteListener {
* @param millis milliseconds to pause
*/
void onPause(int millis);

/**
* On state change
*/
void onStateChange(CassetteController.CassetteState state);
}

void load(CassetteListener listener) throws IOException;
void load(PlaybackListener listener) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ public TapLoader(Path path) {
}

@Override
public void load(CassetteListener listener) throws IOException {
public void load(PlaybackListener listener) throws IOException {
try (FileInputStream stream = new FileInputStream(path.toFile())) {
interpret(stream.readAllBytes(), listener);
}
}

private void interpret(byte[] content, CassetteListener listener) {
private void interpret(byte[] content, PlaybackListener listener) {
ByteBuffer buffer = ByteBuffer.wrap(content);
buffer.order(ByteOrder.LITTLE_ENDIAN);
while (buffer.position() < buffer.limit() && !Thread.currentThread().isInterrupted()) {
Expand Down
Loading

0 comments on commit d6f5d13

Please sign in to comment.