mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-25 01:32:55 +00:00
676 lines
16 KiB
C++
676 lines
16 KiB
C++
//
|
||
// Audio.cpp
|
||
// Clock Signal
|
||
//
|
||
// Created by Thomas Harte on 09/11/2021.
|
||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||
//
|
||
|
||
#include "Audio.hpp"
|
||
|
||
#include "Flags.hpp"
|
||
|
||
#include <cassert>
|
||
#include <tuple>
|
||
|
||
using namespace Amiga;
|
||
|
||
Audio::Audio(Chipset &chipset, uint16_t *ram, size_t word_size, float output_rate) :
|
||
DMADevice<4>(chipset, ram, word_size) {
|
||
|
||
// Mark all buffers as available.
|
||
for(auto &flag: buffer_available_) {
|
||
flag.store(true, std::memory_order_relaxed);
|
||
}
|
||
|
||
speaker_.set_input_rate(output_rate);
|
||
speaker_.set_high_frequency_cutoff(7000.0f);
|
||
}
|
||
|
||
// MARK: - Exposed setters.
|
||
|
||
void Audio::set_length(int channel, uint16_t length) {
|
||
assert(channel >= 0 && channel < 4);
|
||
channels_[channel].length = length;
|
||
}
|
||
|
||
void Audio::set_period(int channel, uint16_t period) {
|
||
assert(channel >= 0 && channel < 4);
|
||
channels_[channel].period = period;
|
||
}
|
||
|
||
void Audio::set_volume(int channel, uint16_t volume) {
|
||
assert(channel >= 0 && channel < 4);
|
||
channels_[channel].volume = (volume & 0x40) ? 64 : (volume & 0x3f);
|
||
}
|
||
|
||
template <bool is_external> void Audio::set_data(int channel, uint16_t data) {
|
||
assert(channel >= 0 && channel < 4);
|
||
channels_[channel].wants_data = false;
|
||
channels_[channel].data = data;
|
||
|
||
// TODO: "the [PWM] counter is reset when ... AUDxDAT is written", but
|
||
// does that just mean written by the CPU, or does it include DMA?
|
||
// My guess is the former. But TODO.
|
||
if constexpr (is_external) {
|
||
channels_[channel].reset_output_phase();
|
||
}
|
||
}
|
||
|
||
template void Audio::set_data<false>(int, uint16_t);
|
||
template void Audio::set_data<true>(int, uint16_t);
|
||
|
||
void Audio::set_channel_enables(uint16_t enables) {
|
||
channels_[0].dma_enabled = enables & 1;
|
||
channels_[1].dma_enabled = enables & 2;
|
||
channels_[2].dma_enabled = enables & 4;
|
||
channels_[3].dma_enabled = enables & 8;
|
||
}
|
||
|
||
void Audio::set_modulation_flags(uint16_t flags) {
|
||
channels_[3].attach_period = flags & 0x80;
|
||
channels_[2].attach_period = flags & 0x40;
|
||
channels_[1].attach_period = flags & 0x20;
|
||
channels_[0].attach_period = flags & 0x10;
|
||
|
||
channels_[3].attach_volume = flags & 0x08;
|
||
channels_[2].attach_volume = flags & 0x04;
|
||
channels_[1].attach_volume = flags & 0x02;
|
||
channels_[0].attach_volume = flags & 0x01;
|
||
}
|
||
|
||
void Audio::set_interrupt_requests(uint16_t requests) {
|
||
channels_[0].interrupt_pending = requests & uint16_t(InterruptFlag::AudioChannel0);
|
||
channels_[1].interrupt_pending = requests & uint16_t(InterruptFlag::AudioChannel1);
|
||
channels_[2].interrupt_pending = requests & uint16_t(InterruptFlag::AudioChannel2);
|
||
channels_[3].interrupt_pending = requests & uint16_t(InterruptFlag::AudioChannel3);
|
||
}
|
||
|
||
// MARK: - DMA and mixing.
|
||
|
||
bool Audio::advance_dma(int channel) {
|
||
if(!channels_[channel].wants_data) {
|
||
return false;
|
||
}
|
||
|
||
if(channels_[channel].should_reload_address) {
|
||
channels_[channel].data_address = pointer_[size_t(channel)];
|
||
channels_[channel].should_reload_address = false;
|
||
}
|
||
|
||
set_data<false>(channel, ram_[channels_[channel].data_address & ram_mask_]);
|
||
|
||
if(channels_[channel].state != Channel::State::WaitingForDummyDMA) {
|
||
++channels_[channel].data_address;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
void Audio::output() {
|
||
constexpr InterruptFlag::FlagT interrupts[] = {
|
||
InterruptFlag::AudioChannel0,
|
||
InterruptFlag::AudioChannel1,
|
||
InterruptFlag::AudioChannel2,
|
||
InterruptFlag::AudioChannel3,
|
||
};
|
||
Channel *const modulands[] = {
|
||
&channels_[1],
|
||
&channels_[2],
|
||
&channels_[3],
|
||
nullptr,
|
||
};
|
||
|
||
for(int c = 0; c < 4; c++) {
|
||
if(channels_[c].output(modulands[c])) {
|
||
posit_interrupt(interrupts[c]);
|
||
}
|
||
}
|
||
|
||
// Spin until the next buffer is available if just entering it for the first time.
|
||
// Contention here should be essentially non-existent.
|
||
if(!sample_pointer_) {
|
||
while(!buffer_available_[buffer_pointer_].load(std::memory_order_relaxed));
|
||
}
|
||
|
||
// Left.
|
||
static_assert(std::tuple_size<AudioBuffer>::value % 2 == 0);
|
||
buffer_[buffer_pointer_][sample_pointer_] = int16_t(
|
||
(
|
||
channels_[1].output_level * channels_[1].output_enabled +
|
||
channels_[2].output_level * channels_[2].output_enabled
|
||
) << 7
|
||
);
|
||
|
||
// Right.
|
||
buffer_[buffer_pointer_][sample_pointer_ + 1] = int16_t(
|
||
(
|
||
channels_[0].output_level * channels_[0].output_enabled +
|
||
channels_[3].output_level * channels_[3].output_enabled
|
||
) << 7
|
||
);
|
||
sample_pointer_ += 2;
|
||
|
||
if(sample_pointer_ == buffer_[buffer_pointer_].size()) {
|
||
const auto &buffer = buffer_[buffer_pointer_];
|
||
auto &flag = buffer_available_[buffer_pointer_];
|
||
|
||
flag.store(false, std::memory_order_release);
|
||
queue_.enqueue([this, &buffer, &flag] {
|
||
speaker_.push(buffer.data(), buffer.size() >> 1);
|
||
flag.store(true, std::memory_order_relaxed);
|
||
});
|
||
|
||
buffer_pointer_ = (buffer_pointer_ + 1) % BufferCount;
|
||
sample_pointer_ = 0;
|
||
}
|
||
}
|
||
|
||
// MARK: - Per-channel logic.
|
||
|
||
/*
|
||
Big spiel on the state machine:
|
||
|
||
Commodore's Hardware Rerefence Manual provides the audio subsystem's state
|
||
machine, so I've just tried to reimplement it verbatim. It's depicted
|
||
diagrammatically in the original source as a finite state automata, the
|
||
below is my attempt to translate that into text.
|
||
|
||
|
||
000 State::Disabled:
|
||
|
||
-> State::Disabled (000)
|
||
if: N/A
|
||
action: percntrld
|
||
|
||
-> State::PlayingHigh (010)
|
||
if: AUDDAT, and not AUDxON, and not AUDxIP
|
||
action: percntrld, AUDxIR, volcntrld, pbudld1
|
||
|
||
-> State::WaitingForDummyDMA (001)
|
||
if: AUDxON
|
||
action: percntrld, AUDxDR, lencntrld, dmasen*
|
||
|
||
|
||
* NOTE: except for this case, dmasen is true only when
|
||
LENFIN = 1. Also, AUDxDSR = (AUDxDR and dmasen).
|
||
|
||
|
||
|
||
001 State::WaitingForDummyDMA:
|
||
|
||
-> State::WaitingForDummyDMA (001)
|
||
if: N/A
|
||
action: None
|
||
|
||
-> State::Disabled (000)
|
||
if: not AUDxON
|
||
action: None
|
||
|
||
-> State::WaitingForDMA (101)
|
||
if: AUDxON, and AUDxDAT
|
||
action:
|
||
1. AUDxIR
|
||
2. if not lenfin, then lencount
|
||
|
||
|
||
|
||
101 State::WaitingForDMA:
|
||
|
||
-> State::WaitingForDMA (101)
|
||
if: N/A
|
||
action: None
|
||
|
||
-> State:Disabled (000)
|
||
if: not AUDxON
|
||
action: None
|
||
|
||
-> State::PlayingHigh (010)
|
||
if: AUDxON, and AUDxDAT
|
||
action:
|
||
1. volcntrld, percntrld, pbufld1
|
||
2. if napnav, then AUDxDR
|
||
|
||
|
||
|
||
010 State::PlayingHigh
|
||
|
||
-> State::PlayingHigh (010)
|
||
if: N/A
|
||
action: percount, and penhi
|
||
|
||
-> State::PlayingLow (011)
|
||
if: perfin
|
||
action:
|
||
1. if AUDxAP, then pbufld2
|
||
2. if AUDxAP and AUDxON, then AUDxDR
|
||
3. percntrld
|
||
4. if intreq2 and AUDxON and AUDxAP, then AUDxIR
|
||
5. if AUDxAP and AUDxON, then AUDxIR
|
||
6. if lenfin and AUDxON and AUDxDAT, then lencntrld
|
||
7. if (not lenfin) and AUDxON and AUDxDAT, then lencount
|
||
8. if lenfin and AUDxON and AUDxDAT, then intreq2
|
||
|
||
[note that 6–8 are shared with the Low -> High transition]
|
||
|
||
|
||
|
||
011 State::PlayingLow
|
||
|
||
-> State::PlayingLow (011)
|
||
if: N/A
|
||
action: percount, and not penhi
|
||
|
||
-> State::Disabled (000)
|
||
if: perfin and not (AUDxON or not AUDxIP)
|
||
action: None
|
||
|
||
-> State::PlayingHigh (010)
|
||
if: perfin and (AUDxON or not AUDxIP)
|
||
action:
|
||
1. pbufld1
|
||
2. percntrld
|
||
3. if napnav and AUDxON, then AUDxDR
|
||
4. if napnav and AUDxON and intreq2, AUDxIR
|
||
5. if napnav and not AUDxON, AUDxIR
|
||
6. if lenfin and AUDxON and AUDxDAT, then lencntrld
|
||
7. if (not lenfin) and AUDxON and AUDxDAT, then lencount
|
||
8. if lenfin and AUDxON and AUDxDAT, then intreq2
|
||
|
||
[note that 6-8 are shared with the High -> Low transition]
|
||
|
||
|
||
|
||
Definitions:
|
||
|
||
AUDxON DMA on "x" indicates channel number (signal from DMACON).
|
||
|
||
AUDxIP Audio interrupt pending (input to channel from interrupt circuitry).
|
||
|
||
AUDxIR Audio interrupt request (output from channel to interrupt circuitry).
|
||
|
||
intreq1 Interrupt request that combines with intreq2 to form AUDxIR.
|
||
|
||
intreq2 Prepare for interrupt request. Request comes out after the
|
||
next 011->010 transition in normal operation.
|
||
|
||
AUDxDAT Audio data load signal. Loads 16 bits of data to audio channel.
|
||
|
||
AUDxDR Audio DMA request to Agnus for one word of data.
|
||
|
||
AUDxDSR Audio DMA request to Agnus to reset pointer to start of block.
|
||
|
||
dmasen Restart request enable.
|
||
|
||
percntrld Reload period counter from back-up latch typically written
|
||
by processor with AUDxPER (can also be written by attach mode).
|
||
|
||
percount Count period counter down one latch.
|
||
|
||
perfin Period counter finished (value = 1).
|
||
|
||
lencntrld Reload length counter from back-up latch.
|
||
|
||
lencount Count length counter down one notch.
|
||
|
||
lenfin Length counter finished (value = 1).
|
||
|
||
volcntrld Reload volume counter from back-up latch.
|
||
|
||
pbufld1 Load output buffer from holding latch written to by AUDxDAT.
|
||
|
||
pbufld2 Like pbufld1, but only during 010->011 with attach period.
|
||
|
||
AUDxAV Attach volume. Send data to volume latch of next channel
|
||
instead of to D->A converter.
|
||
|
||
AUDxAP Attach period. Send data to period latch of next channel
|
||
instead of to the D->A converter.
|
||
|
||
penhi Enable the high 8 bits of data to go to the D->A converter.
|
||
|
||
napnav /AUDxAV * /AUDxAP + AUDxAV -- no attach stuff or else attach
|
||
volume. Condition for normal DMA and interrupt requests.
|
||
*/
|
||
|
||
//
|
||
// Non-action fallback transition and setter, plus specialised begin_state declarations.
|
||
//
|
||
|
||
template <Audio::Channel::State end> void Audio::Channel::begin_state(Channel *) {
|
||
state = end;
|
||
}
|
||
template <> void Audio::Channel::begin_state<Audio::Channel::State::PlayingHigh>(Channel *);
|
||
template <> void Audio::Channel::begin_state<Audio::Channel::State::PlayingLow>(Channel *);
|
||
|
||
template <
|
||
Audio::Channel::State begin,
|
||
Audio::Channel::State end> bool Audio::Channel::transit(Channel *moduland) {
|
||
begin_state<end>(moduland);
|
||
return false;
|
||
}
|
||
|
||
//
|
||
// Audio::Channel::State::Disabled
|
||
//
|
||
|
||
template <> bool Audio::Channel::transit<
|
||
Audio::Channel::State::Disabled,
|
||
Audio::Channel::State::PlayingHigh>(Channel *moduland) {
|
||
begin_state<State::PlayingHigh>(moduland);
|
||
|
||
// percntrld
|
||
period_counter = period;
|
||
|
||
// [AUDxIR]: see return result.
|
||
|
||
// volcntrld
|
||
volume_latch = volume;
|
||
reset_output_phase();
|
||
|
||
// pbufld1
|
||
data_latch = data;
|
||
wants_data = true;
|
||
if(moduland && attach_volume) moduland->volume = uint8_t(data_latch);
|
||
|
||
// AUDxIR.
|
||
return true;
|
||
}
|
||
|
||
template <> bool Audio::Channel::transit<
|
||
Audio::Channel::State::Disabled,
|
||
Audio::Channel::State::WaitingForDummyDMA>(Channel *moduland) {
|
||
begin_state<State::WaitingForDummyDMA>(moduland);
|
||
|
||
// percntrld
|
||
period_counter = period;
|
||
|
||
// AUDxDR
|
||
wants_data = true;
|
||
|
||
// lencntrld
|
||
length_counter = length;
|
||
|
||
// dmasen / AUDxDSR
|
||
should_reload_address = true;
|
||
|
||
return false;
|
||
}
|
||
|
||
template <> bool Audio::Channel::output<Audio::Channel::State::Disabled>(Channel *moduland) {
|
||
// if AUDDAT, and not AUDxON, and not AUDxIP.
|
||
if(!wants_data && !dma_enabled && !interrupt_pending) {
|
||
return transit<State::Disabled, State::PlayingHigh>(moduland);
|
||
}
|
||
|
||
// if AUDxON.
|
||
if(dma_enabled) {
|
||
return transit<State::Disabled, State::WaitingForDummyDMA>(moduland);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
//
|
||
// Audio::Channel::State::WaitingForDummyDMA
|
||
//
|
||
|
||
template <> bool Audio::Channel::transit<
|
||
Audio::Channel::State::WaitingForDummyDMA,
|
||
Audio::Channel::State::WaitingForDMA>(Channel *moduland) {
|
||
begin_state<State::WaitingForDMA>(moduland);
|
||
|
||
// AUDxDR
|
||
wants_data = true;
|
||
|
||
// if not lenfin, then lencount
|
||
if(length != 1) {
|
||
-- length_counter;
|
||
}
|
||
|
||
// AUDxIR
|
||
return true;
|
||
}
|
||
|
||
template <> bool Audio::Channel::output<Audio::Channel::State::WaitingForDummyDMA>(Channel *moduland) {
|
||
// if not AUDxON
|
||
if(!dma_enabled) {
|
||
return transit<State::WaitingForDummyDMA, State::Disabled>(moduland);
|
||
}
|
||
|
||
// if AUDxON and AUDxDAT
|
||
if(dma_enabled && !wants_data) {
|
||
return transit<State::WaitingForDummyDMA, State::WaitingForDMA>(moduland);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
//
|
||
// Audio::Channel::State::WaitingForDMA
|
||
//
|
||
|
||
template <> bool Audio::Channel::transit<
|
||
Audio::Channel::State::WaitingForDMA,
|
||
Audio::Channel::State::PlayingHigh>(Channel *moduland) {
|
||
begin_state<State::PlayingHigh>(moduland);
|
||
|
||
// volcntrld
|
||
volume_latch = volume;
|
||
reset_output_phase();
|
||
|
||
// percntrld
|
||
period_counter = period;
|
||
|
||
// pbufld1
|
||
data_latch = data;
|
||
if(moduland && attach_volume) moduland->volume = uint8_t(data_latch);
|
||
|
||
// if napnav
|
||
if(attach_volume || !(attach_volume || attach_period)) {
|
||
// AUDxDR
|
||
wants_data = true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
template <> bool Audio::Channel::output<Audio::Channel::State::WaitingForDMA>(Channel *moduland) {
|
||
// if: not AUDxON
|
||
if(!dma_enabled) {
|
||
return transit<State::WaitingForDummyDMA, State::Disabled>(moduland);
|
||
}
|
||
|
||
// if: AUDxON, and AUDxDAT
|
||
if(dma_enabled && !wants_data) {
|
||
return transit<State::WaitingForDummyDMA, State::PlayingHigh>(moduland);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
//
|
||
// Audio::Channel::State::PlayingHigh
|
||
//
|
||
|
||
void Audio::Channel::decrement_length() {
|
||
// if lenfin and AUDxON and AUDxDAT, then lencntrld
|
||
// if (not lenfin) and AUDxON and AUDxDAT, then lencount
|
||
// if lenfin and AUDxON and AUDxDAT, then intreq2
|
||
if(dma_enabled && !wants_data) {
|
||
-- length_counter;
|
||
|
||
if(!length_counter) {
|
||
length_counter = length;
|
||
will_request_interrupt = true;
|
||
should_reload_address = true; // This feels logical to me; it's a bit
|
||
// of a stab in the dark though.
|
||
}
|
||
}
|
||
}
|
||
|
||
template <> bool Audio::Channel::transit<
|
||
Audio::Channel::State::PlayingHigh,
|
||
Audio::Channel::State::PlayingLow>(Channel *moduland) {
|
||
begin_state<State::PlayingLow>(moduland);
|
||
|
||
bool wants_interrupt = false;
|
||
|
||
// if AUDxAP
|
||
if(attach_period) {
|
||
// pbufld2
|
||
data_latch = data;
|
||
if(moduland) moduland->period = data_latch;
|
||
|
||
// [if AUDxAP] and AUDxON
|
||
if(dma_enabled) {
|
||
// AUDxDR
|
||
wants_data = true;
|
||
|
||
// [if AUDxAP and AUDxON] and intreq2
|
||
if(will_request_interrupt) {
|
||
will_request_interrupt = false;
|
||
|
||
// AUDxIR
|
||
wants_interrupt = true;
|
||
}
|
||
} else {
|
||
// i.e. if AUDxAP and AUDxON, then AUDxIR
|
||
wants_interrupt = true;
|
||
}
|
||
}
|
||
|
||
// percntrld
|
||
period_counter = period;
|
||
|
||
decrement_length();
|
||
|
||
return wants_interrupt;
|
||
}
|
||
|
||
template <> void Audio::Channel::begin_state<Audio::Channel::State::PlayingHigh>(Channel *) {
|
||
state = Audio::Channel::State::PlayingHigh;
|
||
|
||
// penhi.
|
||
output_level = int8_t(data_latch >> 8);
|
||
}
|
||
|
||
template <> bool Audio::Channel::output<Audio::Channel::State::PlayingHigh>(Channel *moduland) {
|
||
// This is a reasonable guess as to the exit condition for this node;
|
||
// Commodore doesn't document.
|
||
if(period_counter == 1) {
|
||
return transit<State::PlayingHigh, State::PlayingLow>(moduland);
|
||
}
|
||
|
||
// percount.
|
||
-- period_counter;
|
||
|
||
return false;
|
||
}
|
||
|
||
//
|
||
// Audio::Channel::State::PlayingLow
|
||
//
|
||
|
||
template <> bool Audio::Channel::transit<
|
||
Audio::Channel::State::PlayingLow,
|
||
Audio::Channel::State::Disabled>(Channel *moduland) {
|
||
begin_state<State::Disabled>(moduland);
|
||
|
||
// Clear the slightly nebulous 'if intreq2 occurred' state.
|
||
will_request_interrupt = false;
|
||
|
||
return false;
|
||
}
|
||
|
||
template <> bool Audio::Channel::transit<
|
||
Audio::Channel::State::PlayingLow,
|
||
Audio::Channel::State::PlayingHigh>(Channel *moduland) {
|
||
begin_state<State::PlayingHigh>(moduland);
|
||
|
||
bool wants_interrupt = false;
|
||
|
||
// volcntrld
|
||
volume_latch = volume;
|
||
reset_output_phase(); // Is this correct?
|
||
|
||
// percntrld
|
||
period_counter = period;
|
||
|
||
// pbufld1
|
||
data_latch = data;
|
||
if(moduland && attach_volume) moduland->volume = uint8_t(data_latch);
|
||
|
||
// if napnav
|
||
if(attach_volume || !(attach_volume || attach_period)) {
|
||
// [if napnav] and AUDxON
|
||
if(dma_enabled) {
|
||
// AUDxDR
|
||
wants_data = true;
|
||
|
||
// [if napnav and AUDxON] and intreq2
|
||
if(will_request_interrupt) {
|
||
will_request_interrupt = false;
|
||
wants_interrupt = true;
|
||
}
|
||
} else {
|
||
// AUDxIR
|
||
wants_interrupt = true;
|
||
}
|
||
}
|
||
|
||
decrement_length();
|
||
|
||
return wants_interrupt;
|
||
}
|
||
|
||
template <> void Audio::Channel::begin_state<Audio::Channel::State::PlayingLow>(Channel *) {
|
||
state = Audio::Channel::State::PlayingLow;
|
||
|
||
// Output low byte.
|
||
output_level = int8_t(data_latch & 0xff);
|
||
}
|
||
|
||
template <> bool Audio::Channel::output<Audio::Channel::State::PlayingLow>(Channel *moduland) {
|
||
-- period_counter;
|
||
|
||
if(!period_counter) {
|
||
const bool dma_or_no_interrupt = dma_enabled || !interrupt_pending;
|
||
if(dma_or_no_interrupt) {
|
||
return transit<State::PlayingLow, State::PlayingHigh>(moduland);
|
||
} else {
|
||
return transit<State::PlayingLow, State::Disabled>(moduland);
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
//
|
||
// Dispatcher
|
||
//
|
||
|
||
bool Audio::Channel::output(Channel *moduland) {
|
||
// Update pulse-width modulation.
|
||
output_phase = output_phase + 1;
|
||
if(output_phase == 64) {
|
||
reset_output_phase();
|
||
} else {
|
||
output_enabled &= output_phase != volume_latch;
|
||
}
|
||
|
||
switch(state) {
|
||
case State::Disabled: return output<State::Disabled>(moduland);
|
||
case State::WaitingForDummyDMA: return output<State::WaitingForDummyDMA>(moduland);
|
||
case State::WaitingForDMA: return output<State::WaitingForDMA>(moduland);
|
||
case State::PlayingHigh: return output<State::PlayingHigh>(moduland);
|
||
case State::PlayingLow: return output<State::PlayingLow>(moduland);
|
||
|
||
default:
|
||
assert(false);
|
||
break;
|
||
}
|
||
|
||
return false;
|
||
}
|