diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c6b9df0..5cf92080 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,7 +133,7 @@ if(UNIX) target_compile_options(sfizz PRIVATE -fno-rtti -fno-exceptions) target_link_libraries(sfizz stdc++fs) endif(UNIX) -target_link_libraries(sfizz absl::strings sndfile absl::flat_hash_map readerwriterqueue absl::flags_parse) +target_link_libraries(sfizz jack absl::strings sndfile absl::flat_hash_map readerwriterqueue absl::flags_parse) ############################### # Test application diff --git a/sources/Globals.h b/sources/Globals.h index 4bd0768c..f3d54bf6 100644 --- a/sources/Globals.h +++ b/sources/Globals.h @@ -7,7 +7,7 @@ namespace config { constexpr double defaultSampleRate { 48000 }; constexpr int defaultSamplesPerBlock { 1024 }; - constexpr int preloadSize { 32768 }; + constexpr int preloadSize { 8192 }; constexpr int numChannels { 2 }; constexpr int numVoices { 64 }; constexpr int numLoadingThreads { 4 }; diff --git a/sources/Main.cpp b/sources/Main.cpp index b512dfe4..7f99893c 100644 --- a/sources/Main.cpp +++ b/sources/Main.cpp @@ -1,21 +1,152 @@ +#include "StereoSpan.h" #include "Synth.h" -#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std::literals; + +static jack_port_t* midiInputPort; +static jack_port_t* outputPort1; +static jack_port_t* outputPort2; +static jack_client_t* client; + +namespace midi { +constexpr uint8_t statusMask { 0b11110000 }; +constexpr uint8_t channelMask { 0b00001111 }; +constexpr uint8_t noteOff { 0x80 }; +constexpr uint8_t noteOn { 0x90 }; +constexpr uint8_t polyphonicPressure { 0xA0 }; +constexpr uint8_t controlChange { 0xB0 }; +constexpr uint8_t programChange { 0xC0 }; +constexpr uint8_t channelPressure { 0xD0 }; +constexpr uint8_t pitchBend { 0xE0 }; +constexpr uint8_t systemMessage { 0xF0 }; + +constexpr uint8_t status(uint8_t midiStatusByte) +{ + return midiStatusByte & statusMask; +} +constexpr uint8_t channel(uint8_t midiStatusByte) +{ + return midiStatusByte & channelMask; +} +} + +static std::atomic keepRunning [[maybe_unused]] { true }; + +int process(jack_nframes_t numFrames, void* arg [[maybe_unused]]) +{ + auto synth = reinterpret_cast(arg); + + auto buffer = jack_port_get_buffer(midiInputPort, numFrames); + assert(buffer); + + auto numMidiEvents = jack_midi_get_event_count(buffer); + jack_midi_event_t event; + + for (uint32_t i = 0; i < numMidiEvents; ++i) { + if (jack_midi_event_get(&event, buffer, i) != 0) + continue; + + if (event.size == 0) + continue; + + switch (midi::status(event.buffer[0])) { + case midi::noteOff: + DBG("[MIDI] Note " << event.buffer[1] << " OFF at time " << event.time); + synth->noteOff(event.time, midi::channel(event.buffer[0]), event.buffer[1], event.buffer[2]); + break; + case midi::noteOn: + DBG("[MIDI] Note " << event.buffer[1] << " ON at time " << event.time); + synth->noteOn(event.time, midi::channel(event.buffer[0]), event.buffer[1], event.buffer[2]); + break; + case midi::polyphonicPressure: + DBG("[MIDI] Polyphonic pressure on at time " << event.time); + break; + case midi::controlChange: + DBG("[MIDI] CC " << event.buffer[1] << " at time " << event.time); + synth->cc(event.time, midi::channel(event.buffer[0]), event.buffer[1], event.buffer[2]); + break; + case midi::programChange: + DBG("[MIDI] Program change at time " << event.time); + break; + case midi::channelPressure: + DBG("[MIDI] Channel pressure at time " << event.time); + break; + case midi::pitchBend: + DBG("[MIDI] Pitch bend at time " << event.time); + break; + case midi::systemMessage: + DBG("[MIDI] System message at time " << event.time); + break; + } + } + + StereoSpan output { + jack_port_get_buffer(outputPort1, numFrames), + jack_port_get_buffer(outputPort2, numFrames), + numFrames + }; + + synth->renderBlock(output); + + return 0; +} + +int sampleBlockChanged(jack_nframes_t nframes, void* arg [[maybe_unused]]) +{ + if (arg == nullptr) + return 0; + + auto synth = reinterpret_cast(arg); + DBG("Sample per block changed to " << nframes); + synth->setSamplesPerBlock(nframes); + return 0; +} + +int sampleRateChanged(jack_nframes_t nframes, void* arg [[maybe_unused]]) +{ + if (arg == nullptr) + return 0; + + auto synth = reinterpret_cast(arg); + DBG("Sample rate changed to " << nframes); + synth->setSampleRate(nframes); + return 0; +} + +static void done(int sig [[maybe_unused]]) +{ + std::cout << "Closing..." << '\n'; + if (client != nullptr) + jack_client_close(client); + exit(0); +} int main(int argc, char** argv) { - std::ios::sync_with_stdio(false); + // std::ios::sync_with_stdio(false); auto arguments = absl::ParseCommandLine(argc, argv); auto filesToParse = absl::MakeConstSpan(arguments).subspan(1); std::cout << "Positional arguments:"; - for (auto& file: filesToParse) + for (auto& file : filesToParse) std::cout << " " << file << ','; std::cout << '\n'; sfz::Synth synth; std::filesystem::path filename { filesToParse[0] }; - synth.loadSfzFile(filename); + synth.loadSfzFile(filename); std::cout << "==========" << '\n'; std::cout << "Total:" << '\n'; std::cout << "\tMasters: " << synth.getNumMasters() << '\n'; @@ -25,16 +156,80 @@ int main(int argc, char** argv) std::cout << "\tPreloadedSamples: " << synth.getNumPreloadedSamples() << '\n'; std::cout << "==========" << '\n'; std::cout << "Included files:" << '\n'; - for (auto& file: synth.getIncludedFiles()) + for (auto& file : synth.getIncludedFiles()) std::cout << '\t' << file.string() << '\n'; std::cout << "==========" << '\n'; std::cout << "Defines:" << '\n'; - for (auto& define: synth.getDefines()) + for (auto& define : synth.getDefines()) std::cout << '\t' << define.first << '=' << define.second << '\n'; std::cout << "==========" << '\n'; std::cout << "Unknown opcodes:"; - for (auto& opcode: synth.getUnknownOpcodes()) + for (auto& opcode : synth.getUnknownOpcodes()) std::cout << opcode << ','; std::cout << '\n'; + // std::cout << std::flush; + + auto defaultName = "sfizz"; + jack_status_t status; + client = jack_client_open(defaultName, JackNullOption, &status); + if (client == nullptr) { + std::cerr << "Could not open JACK client" << '\n'; + // if (status & JackFailure) + // std::cerr << "JackFailure: Overall operation failed" << '\n'; + return 1; + } + + if (status & JackNameNotUnique) { + std::cout << "Name was taken: assigned " << jack_get_client_name(client) << "instead" << '\n'; + } + if (status & JackServerStarted) { + std::cout << "Connected to JACK" << '\n'; + } + + synth.setSamplesPerBlock(jack_get_buffer_size(client)); + synth.setSampleRate(jack_get_sample_rate(client)); + + jack_set_sample_rate_callback(client, sampleRateChanged, &synth); + jack_set_buffer_size_callback(client, sampleBlockChanged, &synth); + jack_set_process_callback(client, process, &synth); + + midiInputPort = jack_port_register(client, "input", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); + if (midiInputPort == nullptr) { + std::cerr << "Could not open MIDI input port" << '\n'; + return 1; + } + + outputPort1 = jack_port_register(client, "output_1", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + outputPort2 = jack_port_register(client, "output_2", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + if (outputPort1 == nullptr || outputPort2 == nullptr) { + std::cerr << "Could not open output ports" << '\n'; + return 1; + } + + if (jack_activate(client) != 0) { + std::cerr << "Could not activate client" << '\n'; + return 1; + } + + auto systemPorts = jack_get_ports(client, nullptr, nullptr, JackPortIsPhysical | JackPortIsInput); + if (systemPorts == nullptr) { + std::cerr << "No physical output ports found" << '\n'; + return 1; + } + + if (jack_connect(client, jack_port_name(outputPort1), systemPorts[0])) { + std::cerr << "Cannot connect to physical output ports (0)" << '\n'; + } + + if (jack_connect(client, jack_port_name(outputPort2), systemPorts[1])) { + std::cerr << "Cannot connect to physical output ports (1)" << '\n'; + } + jack_free(systemPorts); + + signal(SIGHUP, done); + signal(SIGINT, done); + signal(SIGTERM, done); + signal(SIGQUIT, done); + sleep(-1); return 0; } \ No newline at end of file diff --git a/sources/StereoBuffer.h b/sources/StereoBuffer.h index 145c7503..b16d5f75 100644 --- a/sources/StereoBuffer.h +++ b/sources/StereoBuffer.h @@ -33,17 +33,23 @@ public: return false; } - absl::Span getSpan(Channel channel) const + absl::Span getConstSpan(Channel channel) const { switch (channel) { case Channel::left: return leftBuffer; case Channel::right: return rightBuffer; - // Should not be here by construction... - default: - ASSERTFALSE; - return {}; + } + } + + absl::Span getSpan(Channel channel) + { + switch (channel) { + case Channel::left: + return absl::MakeSpan(leftBuffer); + case Channel::right: + return absl::MakeSpan(rightBuffer); } } @@ -55,10 +61,6 @@ public: return leftBuffer[sampleIndex]; case Channel::right: return rightBuffer[sampleIndex]; - // Should not be here by construction... - default: - ASSERTFALSE; - return trash; } } @@ -152,9 +154,6 @@ private: static constexpr auto TypeAlignmentMask { TypeAlignment - 1 }; static_assert(TypeAlignment * sizeof(Type) == Alignment, "The alignment does not appear to be divided by the size of the Type"); int numFrames { 0 }; - int totalSize { 0 }; - int padding { 0 }; Buffer leftBuffer {}; Buffer rightBuffer {}; - Type trash { 0 }; }; diff --git a/sources/StereoSpan.h b/sources/StereoSpan.h new file mode 100644 index 00000000..c1485c47 --- /dev/null +++ b/sources/StereoSpan.h @@ -0,0 +1,131 @@ +#pragma once +#include "Helpers.h" +#include "SIMDHelpers.h" +#include "StereoBuffer.h" +#include +#include + +template +class StereoSpan { +public: + using ValueType = std::remove_cv_t; + template + StereoSpan() = delete; + StereoSpan(Type* leftBuffer, Type* rightBuffer, size_t numFrames) + : numFrames(numFrames) + , leftBuffer(leftBuffer, numFrames) + , rightBuffer(rightBuffer, numFrames) + { + } + + StereoSpan(void* leftBuffer, void* rightBuffer, size_t numFrames) + : numFrames(numFrames) + , leftBuffer(static_cast(leftBuffer), numFrames) + , rightBuffer(static_cast(rightBuffer), numFrames) + { + } + + StereoSpan(absl::Span leftBuffer, absl::Span rightBuffer) + : numFrames(std::min(leftBuffer.size(), rightBuffer.size())) + , leftBuffer(leftBuffer.first(numFrames)) + , rightBuffer(rightBuffer.first(numFrames)) + { + // Buffer really should be the same size here + ASSERT(leftBuffer.size() == rightBuffer.size()); + } + + StereoSpan(StereoBuffer& buffer) + : leftBuffer(buffer.getConstSpan(Channel::left)) + , rightBuffer(buffer.getConstSpan(Channel::right)) + , numFrames(buffer.getNumFrames()) + { + } + + StereoSpan(StereoBuffer& buffer, size_t numFrames) + : numFrames(numFrames) + , leftBuffer(buffer.getSpan(Channel::left).first(numFrames)) + , rightBuffer(buffer.getSpan(Channel::right).first(numFrames)) + { + } + + StereoSpan(const StereoBuffer& buffer, size_t numFrames) + : numFrames(numFrames) + , leftBuffer(buffer.getConstSpan(Channel::left).first(numFrames)) + , rightBuffer(buffer.getConstSpan(Channel::right).first(numFrames)) + { + } + + StereoSpan(StereoSpan& span) + : numFrames(span.size()) + , leftBuffer(span.left()) + , rightBuffer(span.right()) + { + } + + StereoSpan(const StereoSpan& span) + : numFrames(span.size()) + , leftBuffer(span.left()) + , rightBuffer(span.right()) + { + } + + void fill(Type value) noexcept + { + ::fill(leftBuffer, value); + ::fill(rightBuffer, value); + } + + void readInterleaved(absl::Span input) noexcept + { + ASSERT(input.size() <= static_cast(numChannels * numFrames)); + ::readInterleaved(input, absl::MakeSpan(leftBuffer), absl::MakeSpan(rightBuffer)); + } + + void writeInterleaved(absl::Span output) noexcept + { + ASSERT(output.size() >= static_cast(numChannels * numFrames)); + ::writeInterleaved(leftBuffer, rightBuffer, output); + } + + void add(StereoSpan buffer) + { + ::add(buffer.left(), leftBuffer); + ::add(buffer.right(), rightBuffer); + } + + absl::Span left() + { + return leftBuffer; + } + + absl::Span right() + { + return rightBuffer; + } + + StereoSpan first(size_t length) + { + return { leftBuffer.first(length), rightBuffer.first(length) }; + } + + StereoSpan last(size_t length) + { + return { leftBuffer.last(length), rightBuffer.last(length) }; + } + + StereoSpan subspan(size_t pos, size_t length = absl::Span::npos) + { + return { leftBuffer.subspan(pos, length), rightBuffer.subspan(pos, length) }; + } + + size_t size() + { + return numFrames; + } + +private: + static constexpr int numChannels { 2 }; + size_t numFrames { 0 }; + absl::Span leftBuffer; + absl::Span rightBuffer; +}; \ No newline at end of file diff --git a/sources/Synth.h b/sources/Synth.h index 8d504a92..3a816b01 100644 --- a/sources/Synth.h +++ b/sources/Synth.h @@ -2,6 +2,7 @@ #include "FilePool.h" #include "Parser.h" #include "Region.h" +#include "StereoSpan.h" #include "SfzHelpers.h" #include "absl/types/span.h" #include @@ -28,20 +29,27 @@ public: auto getUnknownOpcodes() { return unknownOpcodes; } size_t getNumPreloadedSamples() { return filePool.getNumPreloadedSamples(); } - void prepareToPlay(int samplesPerBlock, double sampleRate) + void setSamplesPerBlock(int samplesPerBlock) { + DBG("[Synth] Samples per block set to " << samplesPerBlock); this->samplesPerBlock = samplesPerBlock; - this->sampleRate = sampleRate; - this->tempBuffer = StereoBuffer(samplesPerBlock); + this->tempBuffer.resize(samplesPerBlock); } - void renderBlock(StereoBuffer& buffer) + void setSampleRate(double sampleRate) + { + DBG("[Synth] Sample rate set to " << sampleRate); + this->sampleRate = sampleRate; + } + + void renderBlock(StereoSpan buffer) { buffer.fill(0.0f); + StereoSpan tempSpan { tempBuffer, buffer.size() }; for (auto& voice : voices) { - voice->renderBlock(tempBuffer); - buffer.add(tempBuffer); + voice->renderBlock(tempSpan); + buffer.add(tempSpan); } } @@ -83,7 +91,22 @@ public: } } } - void cc(int delay, int channel, int ccNumber, uint8_t ccValue); + void cc(int delay, int channel, int ccNumber, uint8_t ccValue) + { + for (auto& voice : voices) + voice->registerCC(delay, channel, ccNumber, ccValue); + + for (auto& region : regions) { + if (region->registerCC(channel, ccNumber, ccValue)) { + auto voice = findFreeVoice(); + if (voice == nullptr) + continue; + + voice->startVoice(region.get(), channel, ccNumber, ccValue, Voice::TriggerType::NoteOff); + } + } + } + void pitchWheel(int delay, int channel, int pitch); void aftertouch(int delay, int channel, uint8_t aftertouch); void tempo(int delay, float secondsPerQuarter); diff --git a/sources/Voice.h b/sources/Voice.h index 1f316ad1..6dce2e9c 100644 --- a/sources/Voice.h +++ b/sources/Voice.h @@ -1,5 +1,6 @@ #pragma once #include "StereoBuffer.h" +#include "StereoSpan.h" #include "Globals.h" #include "Region.h" #include @@ -32,14 +33,20 @@ public: return (region == nullptr); } - void registerNoteOff(int delay, int channel, int noteNumber, uint8_t velocity); - bool registerCC(int delay, int channel, int ccNumber, uint8_t ccValue); + void registerNoteOff(int delay [[maybe_unused]], int channel [[maybe_unused]], int noteNumber [[maybe_unused]], uint8_t velocity [[maybe_unused]]) + { + + } + void registerCC(int delay [[maybe_unused]], int channel [[maybe_unused]], int ccNumber [[maybe_unused]], uint8_t ccValue [[maybe_unused]]) + { + + } void registerPitchWheel(int delay, int channel, int pitch); void registerAftertouch(int delay, int channel, uint8_t aftertouch); void registerTempo(int delay, float secondsPerQuarter); void prepareToPlay(int samplesPerBlock, double sampleRate); - void renderBlock(StereoBuffer& buffer) + void renderBlock(StereoSpan buffer) { buffer.fill(0.0f); }