Files

253 lines
10 KiB
C++
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "bass.h" // BASS core API
#include "bassmidi.h" // BASSMIDI extension API
#include <iostream>
#include <fstream>
#include <cstring>
#include <iomanip>
#include <thread>
#include <chrono>
void WriteWAVHeader(std::ofstream &file, DWORD length) {
// Ensure the file is open and valid before writing
if (!file.is_open()) {
// Handle error: file is not open
std::cerr << "Error: Output file is not open." << std::endl;
return;
}
DWORD dataChunkSize = length;
DWORD fmtChunkSize = 16; // Size of the fmt chunk for PCM
// Audio format properties (PCM, Stereo, 44100 Hz, 16 bits per sample)
WORD formatTag = 1; // PCM = 1
WORD channels = 2;
DWORD sampleRate = 44100;
WORD bitsPerSample = 16;
// Calculate byte rate and block align
DWORD byteRate = sampleRate * channels * bitsPerSample / 8;
WORD blockAlign = channels * bitsPerSample / 8;
// Calculate the size of the data in the RIFF chunk following the initial 8 bytes.
// This is the size of the "WAVE" identifier (4 bytes) + the size of the "fmt " chunk
// (8 bytes header + fmtChunkSize) + the size of the "data" chunk (8 bytes header + dataChunkSize).
// Total size = 4 (for "WAVE") + (8 + fmtChunkSize) + (8 + dataChunkSize)
// Since fmtChunkSize is 16 for PCM:
// Total size = 4 + (8 + 16) + (8 + dataChunkSize)
// Total size = 4 + 24 + 8 + dataChunkSize
// Total size = 36 + dataChunkSize
DWORD totalSize = 36 + dataChunkSize;
// RIFF chunk
file.write("RIFF", 4);
// Write the total size of the file minus the first 8 bytes ("RIFF" and this size field)
file.write(reinterpret_cast<const char*>(&totalSize), 4);
file.write("WAVE", 4);
// fmt chunk
file.write("fmt ", 4); // Note the trailing space
file.write(reinterpret_cast<const char*>(&fmtChunkSize), 4); // Size of the fmt chunk (16 for PCM)
file.write(reinterpret_cast<const char*>(&formatTag), 2); // Audio format (1 for PCM)
file.write(reinterpret_cast<const char*>(&channels), 2); // Number of channels
file.write(reinterpret_cast<const char*>(&sampleRate), 4); // Sample rate
file.write(reinterpret_cast<const char*>(&byteRate), 4); // Byte rate
file.write(reinterpret_cast<const char*>(&blockAlign), 2); // Block align
file.write(reinterpret_cast<const char*>(&bitsPerSample), 2); // Bits per sample
// data chunk
file.write("data", 4); // Note the trailing space is NOT present for "data"
file.write(reinterpret_cast<const char*>(&dataChunkSize), 4); // Size of the data chunk
// Note: For strict WAV compliance, if dataChunkSize is odd, a padding byte
// should be added here after the data chunk. This function only writes the header.
}
const char* gm_instruments[128] = {
"Acoustic Grand Piano", "Bright Acoustic Piano", "Electric Grand Piano", "Honky-tonk Piano",
"Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet",
"Celesta", "Glockenspiel", "Music Box", "Vibraphone",
"Marimba", "Xylophone", "Tubular Bells", "Dulcimer",
"Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ",
"Reed Organ", "Accordion", "Harmonica", "Tango Accordion",
"Acoustic Guitar (nylon)", "Acoustic Guitar (steel)", "Electric Guitar (jazz)", "Electric Guitar (clean)",
"Electric Guitar (muted)", "Overdriven Guitar", "Distortion Guitar", "Guitar Harmonics",
"Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass",
"Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2",
"Violin", "Viola", "Cello", "Contrabass",
"Tremolo Strings", "Pizzicato Strings", "Orchestral Harp", "Timpani",
"String Ensemble 1", "String Ensemble 2", "SynthStrings 1", "SynthStrings 2",
"Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit",
"Trumpet", "Trombone", "Tuba", "Muted Trumpet",
"French Horn", "Brass Section", "SynthBrass 1", "SynthBrass 2",
"Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax",
"Oboe", "English Horn", "Bassoon", "Clarinet",
"Piccolo", "Flute", "Recorder", "Pan Flute",
"Blown Bottle", "Shakuhachi", "Whistle", "Ocarina",
"Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", "Lead 4 (chiff)",
"Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8 (bass + lead)",
"Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", "Pad 4 (choir)",
"Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)",
"FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)",
"FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", "FX 8 (sci-fi)",
"Sitar", "Banjo", "Shamisen", "Koto",
"Kalimba", "Bagpipe", "Fiddle", "Shanai",
"Tinkle Bell", "Agogo", "Steel Drums", "Woodblock",
"Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal",
"Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet",
"Telephone Ring", "Helicopter", "Applause", "Gunshot"
};
void ShowActiveInstruments(HSTREAM stream) {
for (int ch = 0; ch < 16; ++ch) {
DWORD prog = BASS_MIDI_StreamGetEvent(stream, ch, MIDI_EVENT_PROGRAM);
DWORD bank = BASS_MIDI_StreamGetEvent(stream, ch, MIDI_EVENT_BANK);
if (prog == -1 || prog >= 128) continue;
const char* name = gm_instruments[prog];
std::cout << "> [Ch " << std::setw(2) << std::setfill(' ') << (ch + 1)
<< "] " << name << " (Bank " << bank << ", Prog " << prog << ")\n";
}
}
void ClearLines(int count) {
for (int i = 0; i < count; ++i) {
std::cout << "\x1B[2K" << "\x1B[1A" << "\r"; // erase line + move up
}
std::cout << "\x1B[2K\r"; // erase current line
}
int main(int argc, char** argv) {
std::cout << "Program start\n";
if (argc < 3 || argc > 4) {
std::cerr << "Wrong arguments!\n";
return -1;
}
bool synthToFile = false;
if (argc == 4) {
if (std::strcmp(argv[3], "synth") == 0) {
synthToFile = true;
}
}
// 1) Initialize BASS: default device, 44100 Hz, no flags
if (!BASS_Init(-1, 44100, 0, NULL, NULL)) {
std::cerr << "BASS_Init error: " << BASS_ErrorGetCode() << std::endl;
return 1;
}
// 2) Load the SF2PACK soundfont
HSOUNDFONT sfont = BASS_MIDI_FontInit(argv[1], 0);
if (!sfont) {
std::cerr << "BASS_MIDI_FontInit error: " << BASS_ErrorGetCode() << std::endl;
BASS_Free();
return 1;
}
// Create decoding stream
DWORD flags = BASS_SAMPLE_FLOAT | BASS_MIDI_NOFX;
if (synthToFile)
flags |= BASS_STREAM_DECODE;
HSTREAM midiStream = BASS_MIDI_StreamCreateFile(FALSE, argv[2], 0, 0, flags, 44100);
if (!midiStream) {
std::cerr << "BASS_MIDI_StreamCreateFile error: " << BASS_ErrorGetCode() << std::endl;
BASS_MIDI_FontFree(sfont);
BASS_Free();
return 1;
}
// Assign soundfont
BASS_MIDI_FONT fontConfig{ sfont, -1, 0 };
if (!BASS_MIDI_StreamSetFonts(midiStream, &fontConfig, 1)) {
std::cerr << "BASS_MIDI_StreamSetFonts error: " << BASS_ErrorGetCode() << std::endl;
BASS_StreamFree(midiStream);
BASS_MIDI_FontFree(sfont);
BASS_Free();
return 1;
}
if (!synthToFile) {
std::cout << "Playing live...\n";
int loopCounter = 0;
if (!BASS_ChannelPlay(midiStream, FALSE)) {
std::cerr << "BASS_ChannelPlay error: " << BASS_ErrorGetCode() << std::endl;
BASS_StreamFree(midiStream);
BASS_MIDI_FontFree(sfont);
BASS_Free();
return 1;
}
bool shouldPlay = true;
std::thread monitorThread([&]() {
const int linesPerUpdate = 18; // 16 channels + header/footer
std::cout << "--- Active Instruments ---\n";
ShowActiveInstruments(midiStream);
std::cout << "--------------------------\n";
while (BASS_ChannelIsActive(midiStream) == BASS_ACTIVE_PLAYING && shouldPlay) {
std::this_thread::sleep_for(std::chrono::seconds(1));
ClearLines(linesPerUpdate);
std::cout << "--- Active Instruments ---\n";
ShowActiveInstruments(midiStream);
std::cout << "--------------------------\n";
std::cout.flush();
}
});
std::cout << "Press Enter to stop...\n";
std::cin.get(); // wait for user input
shouldPlay = false;
monitorThread.join();
std::cout << "Playing end.\n";
} else {
std::ofstream wav("output.wav", std::ios::binary);
if (!wav) {
std::cerr << "Can't open output.wav for writing" << std::endl;
return 1;
}
WriteWAVHeader(wav, 0);
DWORD totalBytes = 0;
const int bufferSize = 44100 * 2 * sizeof(float);
float buffer[44100 * 2];
QWORD totalRead = 0;
QWORD totalStreamLength = BASS_ChannelGetLength(midiStream, BASS_POS_BYTE);
std::cout << "Synthesis start\n";
while (true) {
int bytes = BASS_ChannelGetData(midiStream, buffer, bufferSize);
if (bytes <= 0) break;
short pcm[44100 * 2];
for (int i = 0; i < bytes / 4; ++i) {
float sample = buffer[i] * 32767.0f;
if (sample > 32767.0f) sample = 32767.0f;
if (sample < -32768.0f) sample = -32768.0f;
pcm[i] = static_cast<short>(sample);
}
wav.write(reinterpret_cast<const char*>(pcm), bytes / 2);
totalBytes += bytes / 2;
totalRead += bytes;
float percent = (totalStreamLength > 0) ? (100.0f * totalRead / totalStreamLength) : 0.0f;
printf("\rProgress: %.2f%%", percent);
}
wav.seekp(0);
WriteWAVHeader(wav, totalBytes);
wav.close();
std::cout << "\nSynthesis end\n";
std::cout << "Exported to output.wav" << std::endl;
}
// 6) Clean up
BASS_StreamFree(midiStream); // free stream
BASS_MIDI_FontFree(sfont); // unload soundfont
BASS_Free(); // shut down BASS
return 0;
}